1 # Copyright 2011 Peter Arrenbrecht <peter.arrenbrecht@gmail.com> |
|
2 # Logilab SA <contact@logilab.fr> |
|
3 # Pierre-Yves David <pierre-yves.david@ens-lyon.org> |
|
4 # Patrick Mezard <patrick@mezard.eu> |
|
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 '''extends Mercurial feature related to Changeset Evolution |
|
10 |
|
11 This extension provides several commands to mutate history and deal with |
|
12 resulting issues. |
|
13 |
|
14 It also: |
|
15 |
|
16 - enables the "Changeset Obsolescence" feature of Mercurial, |
|
17 - alters core commands and extensions that rewrite history to use |
|
18 this feature, |
|
19 - improves some aspect of the early implementation in Mercurial core |
|
20 ''' |
|
21 |
|
22 __version__ = '5.6.0' |
|
23 testedwith = '3.4.3 3.5.2 3.6.2 3.7.3 3.8.1 3.9 4.0 4.1' |
|
24 buglink = 'https://bz.mercurial-scm.org/' |
|
25 |
|
26 |
|
27 evolutionhelptext = """ |
|
28 Obsolescence markers make it possible to mark changesets that have been |
|
29 deleted or superset in a new version of the changeset. |
|
30 |
|
31 Unlike the previous way of handling such changes, by stripping the old |
|
32 changesets from the repository, obsolescence markers can be propagated |
|
33 between repositories. This allows for a safe and simple way of exchanging |
|
34 mutable history and altering it after the fact. Changeset phases are |
|
35 respected, such that only draft and secret changesets can be altered (see |
|
36 :hg:`help phases` for details). |
|
37 |
|
38 Obsolescence is tracked using "obsolete markers", a piece of metadata |
|
39 tracking which changesets have been made obsolete, potential successors for |
|
40 a given changeset, the moment the changeset was marked as obsolete, and the |
|
41 user who performed the rewriting operation. The markers are stored |
|
42 separately from standard changeset data can be exchanged without any of the |
|
43 precursor changesets, preventing unnecessary exchange of obsolescence data. |
|
44 |
|
45 The complete set of obsolescence markers describes a history of changeset |
|
46 modifications that is orthogonal to the repository history of file |
|
47 modifications. This changeset history allows for detection and automatic |
|
48 resolution of edge cases arising from multiple users rewriting the same part |
|
49 of history concurrently. |
|
50 |
|
51 Current feature status |
|
52 ====================== |
|
53 |
|
54 This feature is still in development. If you see this help, you have enabled an |
|
55 extension that turned this feature on. |
|
56 |
|
57 Obsolescence markers will be exchanged between repositories that explicitly |
|
58 assert support for the obsolescence feature (this can currently only be done |
|
59 via an extension).""".strip() |
|
60 |
|
61 |
|
62 import sys, os |
|
63 import random |
|
64 try: |
|
65 import StringIO as io |
|
66 StringIO = io.StringIO |
|
67 except ImportError: |
|
68 import io |
|
69 StringIO = io.StringIO |
|
70 import re |
|
71 import collections |
|
72 import socket |
|
73 import errno |
|
74 import hashlib |
|
75 import struct |
|
76 sha1re = re.compile(r'\b[0-9a-f]{6,40}\b') |
|
77 |
|
78 import mercurial |
|
79 from mercurial import util |
|
80 from mercurial import repair |
|
81 |
|
82 try: |
|
83 from mercurial import obsolete |
|
84 if not obsolete._enabled: |
|
85 obsolete._enabled = True |
|
86 from mercurial import wireproto |
|
87 gboptslist = getattr(wireproto, 'gboptslist', None) |
|
88 gboptsmap = getattr(wireproto, 'gboptsmap', None) |
|
89 except (ImportError, AttributeError): |
|
90 gboptslist = gboptsmap = None |
|
91 |
|
92 # Flags for enabling optional parts of evolve |
|
93 commandopt = 'allnewcommands' |
|
94 |
|
95 from mercurial import bookmarks as bookmarksmod |
|
96 from mercurial import cmdutil |
|
97 from mercurial import commands |
|
98 from mercurial import context |
|
99 from mercurial import copies |
|
100 from mercurial import error |
|
101 from mercurial import exchange |
|
102 from mercurial import extensions |
|
103 from mercurial import help |
|
104 from mercurial import httppeer |
|
105 from mercurial import hg |
|
106 from mercurial import lock as lockmod |
|
107 from mercurial import merge |
|
108 from mercurial import node |
|
109 from mercurial import phases |
|
110 from mercurial import patch |
|
111 from mercurial import revset |
|
112 from mercurial import scmutil |
|
113 from mercurial import templatekw |
|
114 from mercurial.i18n import _ |
|
115 from mercurial.commands import walkopts, commitopts, commitopts2, mergetoolopts |
|
116 from mercurial.node import nullid |
|
117 from mercurial import wireproto |
|
118 from mercurial import localrepo |
|
119 from mercurial.hgweb import hgweb_mod |
|
120 |
|
121 cmdtable = {} |
|
122 command = cmdutil.command(cmdtable) |
|
123 |
|
124 _pack = struct.pack |
|
125 _unpack = struct.unpack |
|
126 |
|
127 if gboptsmap is not None: |
|
128 memfilectx = context.memfilectx |
|
129 elif gboptslist is not None: |
|
130 oldmemfilectx = context.memfilectx |
|
131 def memfilectx(repo, *args, **kwargs): |
|
132 return oldmemfilectx(*args, **kwargs) |
|
133 else: |
|
134 raise ImportError('evolve needs version %s or above' % |
|
135 min(testedwith.split())) |
|
136 |
|
137 aliases, entry = cmdutil.findcmd('commit', commands.table) |
|
138 hasinteractivemode = any(['interactive' in e for e in entry[1]]) |
|
139 if hasinteractivemode: |
|
140 interactiveopt = [['i', 'interactive', None, _('use interactive mode')]] |
|
141 else: |
|
142 interactiveopt = [] |
|
143 # This extension contains the following code |
|
144 # |
|
145 # - Extension Helper code |
|
146 # - Obsolescence cache |
|
147 # - ... |
|
148 # - Older format compat |
|
149 |
|
150 |
|
151 ##################################################################### |
|
152 ### Extension helper ### |
|
153 ##################################################################### |
|
154 |
|
155 class exthelper(object): |
|
156 """Helper for modular extension setup |
|
157 |
|
158 A single helper should be instantiated for each extension. Helper |
|
159 methods are then used as decorators for various purpose. |
|
160 |
|
161 All decorators return the original function and may be chained. |
|
162 """ |
|
163 |
|
164 def __init__(self): |
|
165 self._uicallables = [] |
|
166 self._extcallables = [] |
|
167 self._repocallables = [] |
|
168 self._revsetsymbols = [] |
|
169 self._templatekws = [] |
|
170 self._commandwrappers = [] |
|
171 self._extcommandwrappers = [] |
|
172 self._functionwrappers = [] |
|
173 self._duckpunchers = [] |
|
174 |
|
175 def final_uisetup(self, ui): |
|
176 """Method to be used as the extension uisetup |
|
177 |
|
178 The following operations belong here: |
|
179 |
|
180 - Changes to ui.__class__ . The ui object that will be used to run the |
|
181 command has not yet been created. Changes made here will affect ui |
|
182 objects created after this, and in particular the ui that will be |
|
183 passed to runcommand |
|
184 - Command wraps (extensions.wrapcommand) |
|
185 - Changes that need to be visible to other extensions: because |
|
186 initialization occurs in phases (all extensions run uisetup, then all |
|
187 run extsetup), a change made here will be visible to other extensions |
|
188 during extsetup |
|
189 - Monkeypatch or wrap function (extensions.wrapfunction) of dispatch |
|
190 module members |
|
191 - Setup of pre-* and post-* hooks |
|
192 - pushkey setup |
|
193 """ |
|
194 for cont, funcname, func in self._duckpunchers: |
|
195 setattr(cont, funcname, func) |
|
196 for command, wrapper, opts in self._commandwrappers: |
|
197 entry = extensions.wrapcommand(commands.table, command, wrapper) |
|
198 if opts: |
|
199 for short, long, val, msg in opts: |
|
200 entry[1].append((short, long, val, msg)) |
|
201 for cont, funcname, wrapper in self._functionwrappers: |
|
202 extensions.wrapfunction(cont, funcname, wrapper) |
|
203 for c in self._uicallables: |
|
204 c(ui) |
|
205 |
|
206 def final_extsetup(self, ui): |
|
207 """Method to be used as a the extension extsetup |
|
208 |
|
209 The following operations belong here: |
|
210 |
|
211 - Changes depending on the status of other extensions. (if |
|
212 extensions.find('mq')) |
|
213 - Add a global option to all commands |
|
214 - Register revset functions |
|
215 """ |
|
216 knownexts = {} |
|
217 for name, symbol in self._revsetsymbols: |
|
218 revset.symbols[name] = symbol |
|
219 for name, kw in self._templatekws: |
|
220 templatekw.keywords[name] = kw |
|
221 for ext, command, wrapper, opts in self._extcommandwrappers: |
|
222 if ext not in knownexts: |
|
223 try: |
|
224 e = extensions.find(ext) |
|
225 except KeyError: |
|
226 # Extension isn't enabled, so don't bother trying to wrap |
|
227 # it. |
|
228 continue |
|
229 knownexts[ext] = e.cmdtable |
|
230 entry = extensions.wrapcommand(knownexts[ext], command, wrapper) |
|
231 if opts: |
|
232 for short, long, val, msg in opts: |
|
233 entry[1].append((short, long, val, msg)) |
|
234 |
|
235 for c in self._extcallables: |
|
236 c(ui) |
|
237 |
|
238 def final_reposetup(self, ui, repo): |
|
239 """Method to be used as the extension reposetup |
|
240 |
|
241 The following operations belong here: |
|
242 |
|
243 - All hooks but pre-* and post-* |
|
244 - Modify configuration variables |
|
245 - Changes to repo.__class__, repo.dirstate.__class__ |
|
246 """ |
|
247 for c in self._repocallables: |
|
248 c(ui, repo) |
|
249 |
|
250 def uisetup(self, call): |
|
251 """Decorated function will be executed during uisetup |
|
252 |
|
253 example:: |
|
254 |
|
255 @eh.uisetup |
|
256 def setupbabar(ui): |
|
257 print 'this is uisetup!' |
|
258 """ |
|
259 self._uicallables.append(call) |
|
260 return call |
|
261 |
|
262 def extsetup(self, call): |
|
263 """Decorated function will be executed during extsetup |
|
264 |
|
265 example:: |
|
266 |
|
267 @eh.extsetup |
|
268 def setupcelestine(ui): |
|
269 print 'this is extsetup!' |
|
270 """ |
|
271 self._extcallables.append(call) |
|
272 return call |
|
273 |
|
274 def reposetup(self, call): |
|
275 """Decorated function will be executed during reposetup |
|
276 |
|
277 example:: |
|
278 |
|
279 @eh.reposetup |
|
280 def setupzephir(ui, repo): |
|
281 print 'this is reposetup!' |
|
282 """ |
|
283 self._repocallables.append(call) |
|
284 return call |
|
285 |
|
286 def revset(self, symbolname): |
|
287 """Decorated function is a revset symbol |
|
288 |
|
289 The name of the symbol must be given as the decorator argument. |
|
290 The symbol is added during `extsetup`. |
|
291 |
|
292 example:: |
|
293 |
|
294 @eh.revset('hidden') |
|
295 def revsetbabar(repo, subset, x): |
|
296 args = revset.getargs(x, 0, 0, 'babar accept no argument') |
|
297 return [r for r in subset if 'babar' in repo[r].description()] |
|
298 """ |
|
299 def dec(symbol): |
|
300 self._revsetsymbols.append((symbolname, symbol)) |
|
301 return symbol |
|
302 return dec |
|
303 |
|
304 |
|
305 def templatekw(self, keywordname): |
|
306 """Decorated function is a template keyword |
|
307 |
|
308 The name of the keyword must be given as the decorator argument. |
|
309 The symbol is added during `extsetup`. |
|
310 |
|
311 example:: |
|
312 |
|
313 @eh.templatekw('babar') |
|
314 def kwbabar(ctx): |
|
315 return 'babar' |
|
316 """ |
|
317 def dec(keyword): |
|
318 self._templatekws.append((keywordname, keyword)) |
|
319 return keyword |
|
320 return dec |
|
321 |
|
322 def wrapcommand(self, command, extension=None, opts=[]): |
|
323 """Decorated function is a command wrapper |
|
324 |
|
325 The name of the command must be given as the decorator argument. |
|
326 The wrapping is installed during `uisetup`. |
|
327 |
|
328 If the second option `extension` argument is provided, the wrapping |
|
329 will be applied in the extension commandtable. This argument must be a |
|
330 string that will be searched using `extension.find` if not found and |
|
331 Abort error is raised. If the wrapping applies to an extension, it is |
|
332 installed during `extsetup`. |
|
333 |
|
334 example:: |
|
335 |
|
336 @eh.wrapcommand('summary') |
|
337 def wrapsummary(orig, ui, repo, *args, **kwargs): |
|
338 ui.note('Barry!') |
|
339 return orig(ui, repo, *args, **kwargs) |
|
340 |
|
341 The `opts` argument allows specifying additional arguments for the |
|
342 command. |
|
343 |
|
344 """ |
|
345 def dec(wrapper): |
|
346 if extension is None: |
|
347 self._commandwrappers.append((command, wrapper, opts)) |
|
348 else: |
|
349 self._extcommandwrappers.append((extension, command, wrapper, |
|
350 opts)) |
|
351 return wrapper |
|
352 return dec |
|
353 |
|
354 def wrapfunction(self, container, funcname): |
|
355 """Decorated function is a function wrapper |
|
356 |
|
357 This function takes two arguments, the container and the name of the |
|
358 function to wrap. The wrapping is performed during `uisetup`. |
|
359 (there is no extension support) |
|
360 |
|
361 example:: |
|
362 |
|
363 @eh.function(discovery, 'checkheads') |
|
364 def wrapfunction(orig, *args, **kwargs): |
|
365 ui.note('His head smashed in and his heart cut out') |
|
366 return orig(*args, **kwargs) |
|
367 """ |
|
368 def dec(wrapper): |
|
369 self._functionwrappers.append((container, funcname, wrapper)) |
|
370 return wrapper |
|
371 return dec |
|
372 |
|
373 def addattr(self, container, funcname): |
|
374 """Decorated function is to be added to the container |
|
375 |
|
376 This function takes two arguments, the container and the name of the |
|
377 function to wrap. The wrapping is performed during `uisetup`. |
|
378 |
|
379 example:: |
|
380 |
|
381 @eh.function(context.changectx, 'babar') |
|
382 def babar(ctx): |
|
383 return 'babar' in ctx.description |
|
384 """ |
|
385 def dec(func): |
|
386 self._duckpunchers.append((container, funcname, func)) |
|
387 return func |
|
388 return dec |
|
389 |
|
390 eh = exthelper() |
|
391 uisetup = eh.final_uisetup |
|
392 extsetup = eh.final_extsetup |
|
393 reposetup = eh.final_reposetup |
|
394 |
|
395 ##################################################################### |
|
396 ### Option configuration ### |
|
397 ##################################################################### |
|
398 |
|
399 @eh.reposetup # must be the first of its kin. |
|
400 def _configureoptions(ui, repo): |
|
401 # If no capabilities are specified, enable everything. |
|
402 # This is so existing evolve users don't need to change their config. |
|
403 evolveopts = ui.configlist('experimental', 'evolution') |
|
404 if not evolveopts: |
|
405 evolveopts = ['all'] |
|
406 ui.setconfig('experimental', 'evolution', evolveopts, 'evolve') |
|
407 |
|
408 @eh.uisetup |
|
409 def _configurecmdoptions(ui): |
|
410 # Unregister evolve commands if the command capability is not specified. |
|
411 # |
|
412 # This must be in the same function as the option configuration above to |
|
413 # guarantee it happens after the above configuration, but before the |
|
414 # extsetup functions. |
|
415 evolvecommands = ui.configlist('experimental', 'evolutioncommands') |
|
416 evolveopts = ui.configlist('experimental', 'evolution') |
|
417 if evolveopts and (commandopt not in evolveopts and |
|
418 'all' not in evolveopts): |
|
419 # We build whitelist containing the commands we want to enable |
|
420 whitelist = set() |
|
421 for cmd in evolvecommands: |
|
422 matchingevolvecommands = [e for e in cmdtable.keys() if cmd in e] |
|
423 if not matchingevolvecommands: |
|
424 raise error.Abort(_('unknown command: %s') % cmd) |
|
425 elif len(matchingevolvecommands) > 1: |
|
426 msg = _('ambiguous command specification: "%s" matches %r') |
|
427 raise error.Abort(msg % (cmd, matchingevolvecommands)) |
|
428 else: |
|
429 whitelist.add(matchingevolvecommands[0]) |
|
430 for disabledcmd in set(cmdtable) - whitelist: |
|
431 del cmdtable[disabledcmd] |
|
432 |
|
433 ##################################################################### |
|
434 ### experimental behavior ### |
|
435 ##################################################################### |
|
436 |
|
437 commitopts3 = [ |
|
438 ('D', 'current-date', None, |
|
439 _('record the current date as commit date')), |
|
440 ('U', 'current-user', None, |
|
441 _('record the current user as committer')), |
|
442 ] |
|
443 |
|
444 def _resolveoptions(ui, opts): |
|
445 """modify commit options dict to handle related options |
|
446 |
|
447 For now, all it does is figure out the commit date: respect -D unless |
|
448 -d was supplied. |
|
449 """ |
|
450 # N.B. this is extremely similar to setupheaderopts() in mq.py |
|
451 if not opts.get('date') and opts.get('current_date'): |
|
452 opts['date'] = '%d %d' % util.makedate() |
|
453 if not opts.get('user') and opts.get('current_user'): |
|
454 opts['user'] = ui.username() |
|
455 |
|
456 getrevs = obsolete.getrevs |
|
457 |
|
458 ##################################################################### |
|
459 ### Additional Utilities ### |
|
460 ##################################################################### |
|
461 |
|
462 # This section contains a lot of small utility function and method |
|
463 |
|
464 # - Function to create markers |
|
465 # - useful alias pstatus and pdiff (should probably go in evolve) |
|
466 # - "troubles" method on changectx |
|
467 # - function to travel through the obsolescence graph |
|
468 # - function to find useful changeset to stabilize |
|
469 |
|
470 |
|
471 ### Useful alias |
|
472 |
|
473 @eh.uisetup |
|
474 def _installalias(ui): |
|
475 if ui.config('alias', 'pstatus', None) is None: |
|
476 ui.setconfig('alias', 'pstatus', 'status --rev .^', 'evolve') |
|
477 if ui.config('alias', 'pdiff', None) is None: |
|
478 ui.setconfig('alias', 'pdiff', 'diff --rev .^', 'evolve') |
|
479 if ui.config('alias', 'olog', None) is None: |
|
480 ui.setconfig('alias', 'olog', "log -r 'precursors(.)' --hidden", |
|
481 'evolve') |
|
482 if ui.config('alias', 'odiff', None) is None: |
|
483 ui.setconfig('alias', 'odiff', |
|
484 "diff --hidden --rev 'limit(precursors(.),1)' --rev .", |
|
485 'evolve') |
|
486 if ui.config('alias', 'grab', None) is None: |
|
487 if os.name == 'nt': |
|
488 ui.setconfig('alias', 'grab', |
|
489 "! " + util.hgexecutable() + " rebase --dest . --rev $@ && " |
|
490 + util.hgexecutable() + " up tip", |
|
491 'evolve') |
|
492 else: |
|
493 ui.setconfig('alias', 'grab', |
|
494 "! $HG rebase --dest . --rev $@ && $HG up tip", |
|
495 'evolve') |
|
496 |
|
497 |
|
498 ### Troubled revset symbol |
|
499 |
|
500 @eh.revset('troubled') |
|
501 def revsettroubled(repo, subset, x): |
|
502 """``troubled()`` |
|
503 Changesets with troubles. |
|
504 """ |
|
505 revset.getargs(x, 0, 0, 'troubled takes no arguments') |
|
506 troubled = set() |
|
507 troubled.update(getrevs(repo, 'unstable')) |
|
508 troubled.update(getrevs(repo, 'bumped')) |
|
509 troubled.update(getrevs(repo, 'divergent')) |
|
510 troubled = revset.baseset(troubled) |
|
511 troubled.sort() # set is non-ordered, enforce order |
|
512 return subset & troubled |
|
513 |
|
514 ### Obsolescence graph |
|
515 |
|
516 # XXX SOME MAJOR CLEAN UP TO DO HERE XXX |
|
517 |
|
518 def _precursors(repo, s): |
|
519 """Precursor of a changeset""" |
|
520 cs = set() |
|
521 nm = repo.changelog.nodemap |
|
522 markerbysubj = repo.obsstore.precursors |
|
523 node = repo.changelog.node |
|
524 for r in s: |
|
525 for p in markerbysubj.get(node(r), ()): |
|
526 pr = nm.get(p[0]) |
|
527 if pr is not None: |
|
528 cs.add(pr) |
|
529 cs -= repo.changelog.filteredrevs # nodemap has no filtering |
|
530 return cs |
|
531 |
|
532 def _allprecursors(repo, s): # XXX we need a better naming |
|
533 """transitive precursors of a subset""" |
|
534 node = repo.changelog.node |
|
535 toproceed = [node(r) for r in s] |
|
536 seen = set() |
|
537 allsubjects = repo.obsstore.precursors |
|
538 while toproceed: |
|
539 nc = toproceed.pop() |
|
540 for mark in allsubjects.get(nc, ()): |
|
541 np = mark[0] |
|
542 if np not in seen: |
|
543 seen.add(np) |
|
544 toproceed.append(np) |
|
545 nm = repo.changelog.nodemap |
|
546 cs = set() |
|
547 for p in seen: |
|
548 pr = nm.get(p) |
|
549 if pr is not None: |
|
550 cs.add(pr) |
|
551 cs -= repo.changelog.filteredrevs # nodemap has no filtering |
|
552 return cs |
|
553 |
|
554 def _successors(repo, s): |
|
555 """Successors of a changeset""" |
|
556 cs = set() |
|
557 node = repo.changelog.node |
|
558 nm = repo.changelog.nodemap |
|
559 markerbyobj = repo.obsstore.successors |
|
560 for r in s: |
|
561 for p in markerbyobj.get(node(r), ()): |
|
562 for sub in p[1]: |
|
563 sr = nm.get(sub) |
|
564 if sr is not None: |
|
565 cs.add(sr) |
|
566 cs -= repo.changelog.filteredrevs # nodemap has no filtering |
|
567 return cs |
|
568 |
|
569 def _allsuccessors(repo, s, haltonflags=0): # XXX we need a better naming |
|
570 """transitive successors of a subset |
|
571 |
|
572 haltonflags allows to provide flags which prevent the evaluation of a |
|
573 marker. """ |
|
574 node = repo.changelog.node |
|
575 toproceed = [node(r) for r in s] |
|
576 seen = set() |
|
577 allobjects = repo.obsstore.successors |
|
578 while toproceed: |
|
579 nc = toproceed.pop() |
|
580 for mark in allobjects.get(nc, ()): |
|
581 if mark[2] & haltonflags: |
|
582 continue |
|
583 for sub in mark[1]: |
|
584 if sub == nullid: |
|
585 continue # should not be here! |
|
586 if sub not in seen: |
|
587 seen.add(sub) |
|
588 toproceed.append(sub) |
|
589 nm = repo.changelog.nodemap |
|
590 cs = set() |
|
591 for s in seen: |
|
592 sr = nm.get(s) |
|
593 if sr is not None: |
|
594 cs.add(sr) |
|
595 cs -= repo.changelog.filteredrevs # nodemap has no filtering |
|
596 return cs |
|
597 |
|
598 |
|
599 |
|
600 |
|
601 ##################################################################### |
|
602 ### Extending revset and template ### |
|
603 ##################################################################### |
|
604 |
|
605 # this section add several useful revset symbol not yet in core. |
|
606 # they are subject to changes |
|
607 |
|
608 |
|
609 ### XXX I'm not sure this revset is useful |
|
610 @eh.revset('suspended') |
|
611 def revsetsuspended(repo, subset, x): |
|
612 """``suspended()`` |
|
613 Obsolete changesets with non-obsolete descendants. |
|
614 """ |
|
615 revset.getargs(x, 0, 0, 'suspended takes no arguments') |
|
616 suspended = revset.baseset(getrevs(repo, 'suspended')) |
|
617 suspended.sort() |
|
618 return subset & suspended |
|
619 |
|
620 |
|
621 @eh.revset('precursors') |
|
622 def revsetprecursors(repo, subset, x): |
|
623 """``precursors(set)`` |
|
624 Immediate precursors of changesets in set. |
|
625 """ |
|
626 s = revset.getset(repo, revset.fullreposet(repo), x) |
|
627 s = revset.baseset(_precursors(repo, s)) |
|
628 s.sort() |
|
629 return subset & s |
|
630 |
|
631 |
|
632 @eh.revset('allprecursors') |
|
633 def revsetallprecursors(repo, subset, x): |
|
634 """``allprecursors(set)`` |
|
635 Transitive precursors of changesets in set. |
|
636 """ |
|
637 s = revset.getset(repo, revset.fullreposet(repo), x) |
|
638 s = revset.baseset(_allprecursors(repo, s)) |
|
639 s.sort() |
|
640 return subset & s |
|
641 |
|
642 |
|
643 @eh.revset('successors') |
|
644 def revsetsuccessors(repo, subset, x): |
|
645 """``successors(set)`` |
|
646 Immediate successors of changesets in set. |
|
647 """ |
|
648 s = revset.getset(repo, revset.fullreposet(repo), x) |
|
649 s = revset.baseset(_successors(repo, s)) |
|
650 s.sort() |
|
651 return subset & s |
|
652 |
|
653 @eh.revset('allsuccessors') |
|
654 def revsetallsuccessors(repo, subset, x): |
|
655 """``allsuccessors(set)`` |
|
656 Transitive successors of changesets in set. |
|
657 """ |
|
658 s = revset.getset(repo, revset.fullreposet(repo), x) |
|
659 s = revset.baseset(_allsuccessors(repo, s)) |
|
660 s.sort() |
|
661 return subset & s |
|
662 |
|
663 ### template keywords |
|
664 # XXX it does not handle troubles well :-/ |
|
665 |
|
666 @eh.templatekw('obsolete') |
|
667 def obsoletekw(repo, ctx, templ, **args): |
|
668 """:obsolete: String. Whether the changeset is ``obsolete``. |
|
669 """ |
|
670 if ctx.obsolete(): |
|
671 return 'obsolete' |
|
672 return '' |
|
673 |
|
674 @eh.templatekw('troubles') |
|
675 def showtroubles(repo, ctx, **args): |
|
676 """:troubles: List of strings. Evolution troubles affecting the changeset |
|
677 (zero or more of "unstable", "divergent" or "bumped").""" |
|
678 return templatekw.showlist('trouble', ctx.troubles(), plural='troubles', |
|
679 **args) |
|
680 |
|
681 ##################################################################### |
|
682 ### Various trouble warning ### |
|
683 ##################################################################### |
|
684 |
|
685 # This section take care of issue warning to the user when troubles appear |
|
686 |
|
687 |
|
688 def _warnobsoletewc(ui, repo): |
|
689 if repo['.'].obsolete(): |
|
690 ui.warn(_('working directory parent is obsolete!\n')) |
|
691 if (not ui.quiet) and obsolete.isenabled(repo, commandopt): |
|
692 ui.warn(_("(use 'hg evolve' to update to its successor)\n")) |
|
693 |
|
694 @eh.wrapcommand("update") |
|
695 @eh.wrapcommand("pull") |
|
696 def wrapmayobsoletewc(origfn, ui, repo, *args, **opts): |
|
697 """Warn that the working directory parent is an obsolete changeset""" |
|
698 def warnobsolete(): |
|
699 _warnobsoletewc(ui, repo) |
|
700 wlock = None |
|
701 try: |
|
702 wlock = repo.wlock() |
|
703 repo._afterlock(warnobsolete) |
|
704 res = origfn(ui, repo, *args, **opts) |
|
705 finally: |
|
706 lockmod.release(wlock) |
|
707 return res |
|
708 |
|
709 @eh.wrapcommand("parents") |
|
710 def wrapparents(origfn, ui, repo, *args, **opts): |
|
711 res = origfn(ui, repo, *args, **opts) |
|
712 _warnobsoletewc(ui, repo) |
|
713 return res |
|
714 |
|
715 # XXX this could wrap transaction code |
|
716 # XXX (but this is a bit a layer violation) |
|
717 @eh.wrapcommand("commit") |
|
718 @eh.wrapcommand("import") |
|
719 @eh.wrapcommand("push") |
|
720 @eh.wrapcommand("pull") |
|
721 @eh.wrapcommand("graft") |
|
722 @eh.wrapcommand("phase") |
|
723 @eh.wrapcommand("unbundle") |
|
724 def warnobserrors(orig, ui, repo, *args, **kwargs): |
|
725 """display warning is the command resulted in more instable changeset""" |
|
726 # part of the troubled stuff may be filtered (stash ?) |
|
727 # This needs a better implementation but will probably wait for core. |
|
728 filtered = repo.changelog.filteredrevs |
|
729 priorunstables = len(set(getrevs(repo, 'unstable')) - filtered) |
|
730 priorbumpeds = len(set(getrevs(repo, 'bumped')) - filtered) |
|
731 priordivergents = len(set(getrevs(repo, 'divergent')) - filtered) |
|
732 ret = orig(ui, repo, *args, **kwargs) |
|
733 # workaround phase stupidity |
|
734 #phases._filterunknown(ui, repo.changelog, repo._phasecache.phaseroots) |
|
735 filtered = repo.changelog.filteredrevs |
|
736 newunstables = \ |
|
737 len(set(getrevs(repo, 'unstable')) - filtered) - priorunstables |
|
738 newbumpeds = \ |
|
739 len(set(getrevs(repo, 'bumped')) - filtered) - priorbumpeds |
|
740 newdivergents = \ |
|
741 len(set(getrevs(repo, 'divergent')) - filtered) - priordivergents |
|
742 if newunstables > 0: |
|
743 ui.warn(_('%i new unstable changesets\n') % newunstables) |
|
744 if newbumpeds > 0: |
|
745 ui.warn(_('%i new bumped changesets\n') % newbumpeds) |
|
746 if newdivergents > 0: |
|
747 ui.warn(_('%i new divergent changesets\n') % newdivergents) |
|
748 return ret |
|
749 |
|
750 @eh.wrapfunction(mercurial.exchange, 'push') |
|
751 def push(orig, repo, *args, **opts): |
|
752 """Add a hint for "hg evolve" when troubles make push fails |
|
753 """ |
|
754 try: |
|
755 return orig(repo, *args, **opts) |
|
756 except error.Abort as ex: |
|
757 hint = _("use 'hg evolve' to get a stable history " |
|
758 "or --force to ignore warnings") |
|
759 if (len(ex.args) >= 1 |
|
760 and ex.args[0].startswith('push includes ') |
|
761 and ex.hint is None): |
|
762 ex.hint = hint |
|
763 raise |
|
764 |
|
765 def summaryhook(ui, repo): |
|
766 def write(fmt, count): |
|
767 s = fmt % count |
|
768 if count: |
|
769 ui.write(s) |
|
770 else: |
|
771 ui.note(s) |
|
772 |
|
773 # util.versiontuple was introduced in 3.6.2 |
|
774 if not util.safehasattr(util, 'versiontuple'): |
|
775 nbunstable = len(getrevs(repo, 'unstable')) |
|
776 nbbumped = len(getrevs(repo, 'bumped')) |
|
777 nbdivergent = len(getrevs(repo, 'divergent')) |
|
778 write('unstable: %i changesets\n', nbunstable) |
|
779 write('bumped: %i changesets\n', nbbumped) |
|
780 write('divergent: %i changesets\n', nbdivergent) |
|
781 else: |
|
782 # In 3.6.2, summary in core gained this feature, no need to display it |
|
783 pass |
|
784 state = _evolvestateread(repo) |
|
785 if state is not None: |
|
786 # i18n: column positioning for "hg summary" |
|
787 ui.write(_('evolve: (evolve --continue)\n')) |
|
788 |
|
789 @eh.extsetup |
|
790 def obssummarysetup(ui): |
|
791 cmdutil.summaryhooks.add('evolve', summaryhook) |
|
792 |
|
793 |
|
794 ##################################################################### |
|
795 ### Core Other extension compat ### |
|
796 ##################################################################### |
|
797 |
|
798 |
|
799 @eh.extsetup |
|
800 def _rebasewrapping(ui): |
|
801 # warning about more obsolete |
|
802 try: |
|
803 rebase = extensions.find('rebase') |
|
804 if rebase: |
|
805 extensions.wrapcommand(rebase.cmdtable, 'rebase', warnobserrors) |
|
806 except KeyError: |
|
807 pass # rebase not found |
|
808 try: |
|
809 histedit = extensions.find('histedit') |
|
810 if histedit: |
|
811 extensions.wrapcommand(histedit.cmdtable, 'histedit', warnobserrors) |
|
812 except KeyError: |
|
813 pass # histedit not found |
|
814 |
|
815 ##################################################################### |
|
816 ### Old Evolve extension content ### |
|
817 ##################################################################### |
|
818 |
|
819 # XXX need clean up and proper sorting in other section |
|
820 |
|
821 ### util function |
|
822 ############################# |
|
823 |
|
824 ### changeset rewriting logic |
|
825 ############################# |
|
826 |
|
827 def rewrite(repo, old, updates, head, newbases, commitopts): |
|
828 """Return (nodeid, created) where nodeid is the identifier of the |
|
829 changeset generated by the rewrite process, and created is True if |
|
830 nodeid was actually created. If created is False, nodeid |
|
831 references a changeset existing before the rewrite call. |
|
832 """ |
|
833 wlock = lock = tr = None |
|
834 try: |
|
835 wlock = repo.wlock() |
|
836 lock = repo.lock() |
|
837 tr = repo.transaction('rewrite') |
|
838 if len(old.parents()) > 1: #XXX remove this unnecessary limitation. |
|
839 raise error.Abort(_('cannot amend merge changesets')) |
|
840 base = old.p1() |
|
841 updatebookmarks = _bookmarksupdater(repo, old.node(), tr) |
|
842 |
|
843 # commit a new version of the old changeset, including the update |
|
844 # collect all files which might be affected |
|
845 files = set(old.files()) |
|
846 for u in updates: |
|
847 files.update(u.files()) |
|
848 |
|
849 # Recompute copies (avoid recording a -> b -> a) |
|
850 copied = copies.pathcopies(base, head) |
|
851 |
|
852 |
|
853 # prune files which were reverted by the updates |
|
854 def samefile(f): |
|
855 if f in head.manifest(): |
|
856 a = head.filectx(f) |
|
857 if f in base.manifest(): |
|
858 b = base.filectx(f) |
|
859 return (a.data() == b.data() |
|
860 and a.flags() == b.flags()) |
|
861 else: |
|
862 return False |
|
863 else: |
|
864 return f not in base.manifest() |
|
865 files = [f for f in files if not samefile(f)] |
|
866 # commit version of these files as defined by head |
|
867 headmf = head.manifest() |
|
868 def filectxfn(repo, ctx, path): |
|
869 if path in headmf: |
|
870 fctx = head[path] |
|
871 flags = fctx.flags() |
|
872 mctx = memfilectx(repo, fctx.path(), fctx.data(), |
|
873 islink='l' in flags, |
|
874 isexec='x' in flags, |
|
875 copied=copied.get(path)) |
|
876 return mctx |
|
877 return None |
|
878 |
|
879 message = cmdutil.logmessage(repo.ui, commitopts) |
|
880 if not message: |
|
881 message = old.description() |
|
882 |
|
883 user = commitopts.get('user') or old.user() |
|
884 date = commitopts.get('date') or None # old.date() |
|
885 extra = dict(commitopts.get('extra', old.extra())) |
|
886 extra['branch'] = head.branch() |
|
887 |
|
888 new = context.memctx(repo, |
|
889 parents=newbases, |
|
890 text=message, |
|
891 files=files, |
|
892 filectxfn=filectxfn, |
|
893 user=user, |
|
894 date=date, |
|
895 extra=extra) |
|
896 |
|
897 if commitopts.get('edit'): |
|
898 new._text = cmdutil.commitforceeditor(repo, new, []) |
|
899 revcount = len(repo) |
|
900 newid = repo.commitctx(new) |
|
901 new = repo[newid] |
|
902 created = len(repo) != revcount |
|
903 updatebookmarks(newid) |
|
904 |
|
905 tr.close() |
|
906 return newid, created |
|
907 finally: |
|
908 lockmod.release(tr, lock, wlock) |
|
909 |
|
910 class MergeFailure(error.Abort): |
|
911 pass |
|
912 |
|
913 def relocate(repo, orig, dest, pctx=None, keepbranch=False): |
|
914 """rewrite <rev> on dest""" |
|
915 if orig.rev() == dest.rev(): |
|
916 raise error.Abort(_('tried to relocate a node on top of itself'), |
|
917 hint=_("This shouldn't happen. If you still " |
|
918 "need to move changesets, please do so " |
|
919 "manually with nothing to rebase - working " |
|
920 "directory parent is also destination")) |
|
921 |
|
922 if pctx is None: |
|
923 if len(orig.parents()) == 2: |
|
924 raise error.Abort(_("tried to relocate a merge commit without " |
|
925 "specifying which parent should be moved"), |
|
926 hint=_("Specify the parent by passing in pctx")) |
|
927 pctx = orig.p1() |
|
928 |
|
929 destbookmarks = repo.nodebookmarks(dest.node()) |
|
930 nodesrc = orig.node() |
|
931 destphase = repo[nodesrc].phase() |
|
932 commitmsg = orig.description() |
|
933 |
|
934 cache = {} |
|
935 sha1s = re.findall(sha1re, commitmsg) |
|
936 unfi = repo.unfiltered() |
|
937 for sha1 in sha1s: |
|
938 ctx = None |
|
939 try: |
|
940 ctx = unfi[sha1] |
|
941 except error.RepoLookupError: |
|
942 continue |
|
943 |
|
944 if not ctx.obsolete(): |
|
945 continue |
|
946 |
|
947 successors = obsolete.successorssets(repo, ctx.node(), cache) |
|
948 |
|
949 # We can't make any assumptions about how to update the hash if the |
|
950 # cset in question was split or diverged. |
|
951 if len(successors) == 1 and len(successors[0]) == 1: |
|
952 newsha1 = node.hex(successors[0][0]) |
|
953 commitmsg = commitmsg.replace(sha1, newsha1[:len(sha1)]) |
|
954 else: |
|
955 repo.ui.note(_('The stale commit message reference to %s could ' |
|
956 'not be updated\n') % sha1) |
|
957 |
|
958 tr = repo.currenttransaction() |
|
959 assert tr is not None |
|
960 try: |
|
961 r = _evolvemerge(repo, orig, dest, pctx, keepbranch) |
|
962 if r[-1]: #some conflict |
|
963 raise error.Abort( |
|
964 'unresolved merge conflicts (see hg help resolve)') |
|
965 nodenew = _relocatecommit(repo, orig, commitmsg) |
|
966 except error.Abort as exc: |
|
967 repo.dirstate.beginparentchange() |
|
968 repo.setparents(repo['.'].node(), nullid) |
|
969 writedirstate(repo.dirstate, tr) |
|
970 # fix up dirstate for copies and renames |
|
971 copies.duplicatecopies(repo, dest.rev(), orig.p1().rev()) |
|
972 repo.dirstate.endparentchange() |
|
973 class LocalMergeFailure(MergeFailure, exc.__class__): |
|
974 pass |
|
975 exc.__class__ = LocalMergeFailure |
|
976 tr.close() # to keep changes in this transaction (e.g. dirstate) |
|
977 raise |
|
978 oldbookmarks = repo.nodebookmarks(nodesrc) |
|
979 _finalizerelocate(repo, orig, dest, nodenew, tr) |
|
980 return nodenew |
|
981 |
|
982 def _bookmarksupdater(repo, oldid, tr): |
|
983 """Return a callable update(newid) updating the current bookmark |
|
984 and bookmarks bound to oldid to newid. |
|
985 """ |
|
986 def updatebookmarks(newid): |
|
987 dirty = False |
|
988 oldbookmarks = repo.nodebookmarks(oldid) |
|
989 if oldbookmarks: |
|
990 for b in oldbookmarks: |
|
991 repo._bookmarks[b] = newid |
|
992 dirty = True |
|
993 if dirty: |
|
994 repo._bookmarks.recordchange(tr) |
|
995 return updatebookmarks |
|
996 |
|
997 ### bookmarks api compatibility layer ### |
|
998 def bmdeactivate(repo): |
|
999 try: |
|
1000 return bookmarksmod.deactivate(repo) |
|
1001 except AttributeError: |
|
1002 return bookmarksmod.unsetcurrent(repo) |
|
1003 def bmactivate(repo, book): |
|
1004 try: |
|
1005 return bookmarksmod.activate(repo, book) |
|
1006 except AttributeError: |
|
1007 return bookmarksmod.setcurrent(repo, book) |
|
1008 |
|
1009 def bmactive(repo): |
|
1010 try: |
|
1011 return repo._activebookmark |
|
1012 except AttributeError: |
|
1013 return repo._bookmarkcurrent |
|
1014 |
|
1015 ### dirstate compatibility layer < hg 3.6 |
|
1016 |
|
1017 def writedirstate(dirstate, tr): |
|
1018 if dirstate.write.func_code.co_argcount != 1: # mercurial 3.6 and above |
|
1019 return dirstate.write(tr) |
|
1020 return dirstate.write() |
|
1021 |
|
1022 |
|
1023 |
|
1024 ### new command |
|
1025 ############################# |
|
1026 metadataopts = [ |
|
1027 ('d', 'date', '', |
|
1028 _('record the specified date in metadata'), _('DATE')), |
|
1029 ('u', 'user', '', |
|
1030 _('record the specified user in metadata'), _('USER')), |
|
1031 ] |
|
1032 |
|
1033 @eh.uisetup |
|
1034 def _installimportobsolete(ui): |
|
1035 entry = cmdutil.findcmd('import', commands.table)[1] |
|
1036 entry[1].append(('', 'obsolete', False, |
|
1037 _('mark the old node as obsoleted by ' |
|
1038 'the created commit'))) |
|
1039 |
|
1040 @eh.wrapfunction(mercurial.cmdutil, 'tryimportone') |
|
1041 def tryimportone(orig, ui, repo, hunk, parents, opts, *args, **kwargs): |
|
1042 extracted = patch.extract(ui, hunk) |
|
1043 if util.safehasattr(extracted, 'get'): |
|
1044 # mercurial 3.6 return a dictionary there |
|
1045 expected = extracted.get('nodeid') |
|
1046 else: |
|
1047 expected = extracted[5] |
|
1048 if expected is not None: |
|
1049 expected = node.bin(expected) |
|
1050 oldextract = patch.extract |
|
1051 try: |
|
1052 patch.extract = lambda ui, hunk: extracted |
|
1053 ret = orig(ui, repo, hunk, parents, opts, *args, **kwargs) |
|
1054 finally: |
|
1055 patch.extract = oldextract |
|
1056 created = ret[1] |
|
1057 if (opts['obsolete'] and None not in (created, expected) |
|
1058 and created != expected): |
|
1059 tr = repo.transaction('import-obs') |
|
1060 try: |
|
1061 metadata = {'user': ui.username()} |
|
1062 repo.obsstore.create(tr, expected, (created,), |
|
1063 metadata=metadata) |
|
1064 tr.close() |
|
1065 finally: |
|
1066 tr.release() |
|
1067 return ret |
|
1068 |
|
1069 |
|
1070 def _deprecatealias(oldalias, newalias): |
|
1071 '''Deprecates an alias for a command in favour of another |
|
1072 |
|
1073 Creates a new entry in the command table for the old alias. It creates a |
|
1074 wrapper that has its synopsis set to show that is has been deprecated. |
|
1075 The documentation will be replace with a pointer to the new alias. |
|
1076 If a user invokes the command a deprecation warning will be printed and |
|
1077 the command of the *new* alias will be invoked. |
|
1078 |
|
1079 This function is loosely based on the extensions.wrapcommand function. |
|
1080 ''' |
|
1081 try: |
|
1082 aliases, entry = cmdutil.findcmd(newalias, cmdtable) |
|
1083 except error.UnknownCommand: |
|
1084 # Commands may be disabled |
|
1085 return |
|
1086 for alias, e in cmdtable.items(): |
|
1087 if e is entry: |
|
1088 break |
|
1089 |
|
1090 synopsis = '(DEPRECATED)' |
|
1091 if len(entry) > 2: |
|
1092 fn, opts, _syn = entry |
|
1093 else: |
|
1094 fn, opts, = entry |
|
1095 deprecationwarning = _('%s have been deprecated in favor of %s\n') % ( |
|
1096 oldalias, newalias) |
|
1097 def newfn(*args, **kwargs): |
|
1098 ui = args[0] |
|
1099 ui.warn(deprecationwarning) |
|
1100 util.checksignature(fn)(*args, **kwargs) |
|
1101 newfn.__doc__ = deprecationwarning |
|
1102 cmdwrapper = command(oldalias, opts, synopsis) |
|
1103 cmdwrapper(newfn) |
|
1104 |
|
1105 @eh.extsetup |
|
1106 def deprecatealiases(ui): |
|
1107 _deprecatealias('gup', 'next') |
|
1108 _deprecatealias('gdown', 'previous') |
|
1109 |
|
1110 @command('debugrecordpruneparents', [], '') |
|
1111 def cmddebugrecordpruneparents(ui, repo): |
|
1112 """add parent data to prune markers when possible |
|
1113 |
|
1114 This command searches the repo for prune markers without parent information. |
|
1115 If the pruned node is locally known, it creates a new marker with parent |
|
1116 data. |
|
1117 """ |
|
1118 pgop = 'reading markers' |
|
1119 |
|
1120 # lock from the beginning to prevent race |
|
1121 wlock = lock = tr = None |
|
1122 try: |
|
1123 wlock = repo.wlock() |
|
1124 lock = repo.lock() |
|
1125 tr = repo.transaction('recordpruneparents') |
|
1126 unfi = repo.unfiltered() |
|
1127 nm = unfi.changelog.nodemap |
|
1128 store = repo.obsstore |
|
1129 pgtotal = len(store._all) |
|
1130 for idx, mark in enumerate(list(store._all)): |
|
1131 if not mark[1]: |
|
1132 rev = nm.get(mark[0]) |
|
1133 if rev is not None: |
|
1134 ctx = unfi[rev] |
|
1135 parents = tuple(p.node() for p in ctx.parents()) |
|
1136 before = len(store._all) |
|
1137 store.create(tr, mark[0], mark[1], mark[2], mark[3], |
|
1138 parents=parents) |
|
1139 if len(store._all) - before: |
|
1140 ui.write(_('created new markers for %i\n') % rev) |
|
1141 ui.progress(pgop, idx, total=pgtotal) |
|
1142 tr.close() |
|
1143 ui.progress(pgop, None) |
|
1144 finally: |
|
1145 lockmod.release(tr, lock, wlock) |
|
1146 |
|
1147 @command('debugobsstorestat', [], '') |
|
1148 def cmddebugobsstorestat(ui, repo): |
|
1149 """print statistics about obsolescence markers in the repo""" |
|
1150 def _updateclustermap(nodes, mark, clustersmap): |
|
1151 c = (set(nodes), set([mark])) |
|
1152 toproceed = set(nodes) |
|
1153 while toproceed: |
|
1154 n = toproceed.pop() |
|
1155 other = clustersmap.get(n) |
|
1156 if (other is not None |
|
1157 and other is not c): |
|
1158 other[0].update(c[0]) |
|
1159 other[1].update(c[1]) |
|
1160 for on in c[0]: |
|
1161 if on in toproceed: |
|
1162 continue |
|
1163 clustersmap[on] = other |
|
1164 c = other |
|
1165 clustersmap[n] = c |
|
1166 |
|
1167 store = repo.obsstore |
|
1168 unfi = repo.unfiltered() |
|
1169 nm = unfi.changelog.nodemap |
|
1170 ui.write(_('markers total: %9i\n') % len(store._all)) |
|
1171 sucscount = [0, 0 , 0, 0] |
|
1172 known = 0 |
|
1173 parentsdata = 0 |
|
1174 metakeys = {} |
|
1175 # node -> cluster mapping |
|
1176 # a cluster is a (set(nodes), set(markers)) tuple |
|
1177 clustersmap = {} |
|
1178 # same data using parent information |
|
1179 pclustersmap = {} |
|
1180 for mark in store: |
|
1181 if mark[0] in nm: |
|
1182 known += 1 |
|
1183 nbsucs = len(mark[1]) |
|
1184 sucscount[min(nbsucs, 3)] += 1 |
|
1185 meta = mark[3] |
|
1186 for key, value in meta: |
|
1187 metakeys.setdefault(key, 0) |
|
1188 metakeys[key] += 1 |
|
1189 meta = dict(meta) |
|
1190 parents = [meta.get('p1'), meta.get('p2')] |
|
1191 parents = [node.bin(p) for p in parents if p is not None] |
|
1192 if parents: |
|
1193 parentsdata += 1 |
|
1194 # cluster handling |
|
1195 nodes = set(mark[1]) |
|
1196 nodes.add(mark[0]) |
|
1197 _updateclustermap(nodes, mark, clustersmap) |
|
1198 # same with parent data |
|
1199 nodes.update(parents) |
|
1200 _updateclustermap(nodes, mark, pclustersmap) |
|
1201 |
|
1202 # freezing the result |
|
1203 for c in clustersmap.values(): |
|
1204 fc = (frozenset(c[0]), frozenset(c[1])) |
|
1205 for n in fc[0]: |
|
1206 clustersmap[n] = fc |
|
1207 # same with parent data |
|
1208 for c in pclustersmap.values(): |
|
1209 fc = (frozenset(c[0]), frozenset(c[1])) |
|
1210 for n in fc[0]: |
|
1211 pclustersmap[n] = fc |
|
1212 ui.write((' for known precursors: %9i\n' % known)) |
|
1213 ui.write((' with parents data: %9i\n' % parentsdata)) |
|
1214 # successors data |
|
1215 ui.write(('markers with no successors: %9i\n' % sucscount[0])) |
|
1216 ui.write((' 1 successors: %9i\n' % sucscount[1])) |
|
1217 ui.write((' 2 successors: %9i\n' % sucscount[2])) |
|
1218 ui.write((' more than 2 successors: %9i\n' % sucscount[3])) |
|
1219 # meta data info |
|
1220 ui.write((' available keys:\n')) |
|
1221 for key in sorted(metakeys): |
|
1222 ui.write((' %15s: %9i\n' % (key, metakeys[key]))) |
|
1223 |
|
1224 allclusters = list(set(clustersmap.values())) |
|
1225 allclusters.sort(key=lambda x: len(x[1])) |
|
1226 ui.write(('disconnected clusters: %9i\n' % len(allclusters))) |
|
1227 |
|
1228 ui.write(' any known node: %9i\n' |
|
1229 % len([c for c in allclusters |
|
1230 if [n for n in c[0] if nm.get(n) is not None]])) |
|
1231 if allclusters: |
|
1232 nbcluster = len(allclusters) |
|
1233 ui.write((' smallest length: %9i\n' % len(allclusters[0][1]))) |
|
1234 ui.write((' longer length: %9i\n' |
|
1235 % len(allclusters[-1][1]))) |
|
1236 median = len(allclusters[nbcluster//2][1]) |
|
1237 ui.write((' median length: %9i\n' % median)) |
|
1238 mean = sum(len(x[1]) for x in allclusters) // nbcluster |
|
1239 ui.write((' mean length: %9i\n' % mean)) |
|
1240 allpclusters = list(set(pclustersmap.values())) |
|
1241 allpclusters.sort(key=lambda x: len(x[1])) |
|
1242 ui.write((' using parents data: %9i\n' % len(allpclusters))) |
|
1243 ui.write(' any known node: %9i\n' |
|
1244 % len([c for c in allclusters |
|
1245 if [n for n in c[0] if nm.get(n) is not None]])) |
|
1246 if allpclusters: |
|
1247 nbcluster = len(allpclusters) |
|
1248 ui.write((' smallest length: %9i\n' |
|
1249 % len(allpclusters[0][1]))) |
|
1250 ui.write((' longer length: %9i\n' |
|
1251 % len(allpclusters[-1][1]))) |
|
1252 median = len(allpclusters[nbcluster//2][1]) |
|
1253 ui.write((' median length: %9i\n' % median)) |
|
1254 mean = sum(len(x[1]) for x in allpclusters) // nbcluster |
|
1255 ui.write((' mean length: %9i\n' % mean)) |
|
1256 |
|
1257 def _solveone(ui, repo, ctx, dryrun, confirm, progresscb, category): |
|
1258 """Resolve the troubles affecting one revision""" |
|
1259 wlock = lock = tr = None |
|
1260 try: |
|
1261 wlock = repo.wlock() |
|
1262 lock = repo.lock() |
|
1263 tr = repo.transaction("evolve") |
|
1264 if 'unstable' == category: |
|
1265 result = _solveunstable(ui, repo, ctx, dryrun, confirm, progresscb) |
|
1266 elif 'bumped' == category: |
|
1267 result = _solvebumped(ui, repo, ctx, dryrun, confirm, progresscb) |
|
1268 elif 'divergent' == category: |
|
1269 result = _solvedivergent(ui, repo, ctx, dryrun, confirm, |
|
1270 progresscb) |
|
1271 else: |
|
1272 assert False, "unknown trouble category: %s" % (category) |
|
1273 tr.close() |
|
1274 return result |
|
1275 finally: |
|
1276 lockmod.release(tr, lock, wlock) |
|
1277 |
|
1278 def _handlenotrouble(ui, repo, allopt, revopt, anyopt, targetcat): |
|
1279 """Used by the evolve function to display an error message when |
|
1280 no troubles can be resolved""" |
|
1281 troublecategories = ['bumped', 'divergent', 'unstable'] |
|
1282 unselectedcategories = [c for c in troublecategories if c != targetcat] |
|
1283 msg = None |
|
1284 hint = None |
|
1285 |
|
1286 troubled = { |
|
1287 "unstable": repo.revs("unstable()"), |
|
1288 "divergent": repo.revs("divergent()"), |
|
1289 "bumped": repo.revs("bumped()"), |
|
1290 "all": repo.revs("troubled()"), |
|
1291 } |
|
1292 |
|
1293 |
|
1294 hintmap = { |
|
1295 'bumped': _("do you want to use --bumped"), |
|
1296 'bumped+divergent': _("do you want to use --bumped or --divergent"), |
|
1297 'bumped+unstable': _("do you want to use --bumped or --unstable"), |
|
1298 'divergent': _("do you want to use --divergent"), |
|
1299 'divergent+unstable': _("do you want to use --divergent" |
|
1300 " or --unstable"), |
|
1301 'unstable': _("do you want to use --unstable"), |
|
1302 'any+bumped': _("do you want to use --any (or --rev) and --bumped"), |
|
1303 'any+bumped+divergent': _("do you want to use --any (or --rev) and" |
|
1304 " --bumped or --divergent"), |
|
1305 'any+bumped+unstable': _("do you want to use --any (or --rev) and" |
|
1306 "--bumped or --unstable"), |
|
1307 'any+divergent': _("do you want to use --any (or --rev) and" |
|
1308 " --divergent"), |
|
1309 'any+divergent+unstable': _("do you want to use --any (or --rev)" |
|
1310 " and --divergent or --unstable"), |
|
1311 'any+unstable': _("do you want to use --any (or --rev)" |
|
1312 "and --unstable"), |
|
1313 } |
|
1314 |
|
1315 if revopt: |
|
1316 revs = scmutil.revrange(repo, revopt) |
|
1317 if not revs: |
|
1318 msg = _("set of specified revisions is empty") |
|
1319 else: |
|
1320 msg = _("no %s changesets in specified revisions") % targetcat |
|
1321 othertroubles = [] |
|
1322 for cat in unselectedcategories: |
|
1323 if revs & troubled[cat]: |
|
1324 othertroubles.append(cat) |
|
1325 if othertroubles: |
|
1326 hint = hintmap['+'.join(othertroubles)] |
|
1327 |
|
1328 elif anyopt: |
|
1329 msg = _("no %s changesets to evolve") % targetcat |
|
1330 othertroubles = [] |
|
1331 for cat in unselectedcategories: |
|
1332 if troubled[cat]: |
|
1333 othertroubles.append(cat) |
|
1334 if othertroubles: |
|
1335 hint = hintmap['+'.join(othertroubles)] |
|
1336 |
|
1337 else: |
|
1338 # evolve without any option = relative to the current wdir |
|
1339 if targetcat == 'unstable': |
|
1340 msg = _("nothing to evolve on current working copy parent") |
|
1341 else: |
|
1342 msg = _("current working copy parent is not %s") % targetcat |
|
1343 |
|
1344 p1 = repo['.'].rev() |
|
1345 othertroubles = [] |
|
1346 for cat in unselectedcategories: |
|
1347 if p1 in troubled[cat]: |
|
1348 othertroubles.append(cat) |
|
1349 if othertroubles: |
|
1350 hint = hintmap['+'.join(othertroubles)] |
|
1351 else: |
|
1352 l = len(troubled[targetcat]) |
|
1353 if l: |
|
1354 hint = _("%d other %s in the repository, do you want --any " |
|
1355 "or --rev") % (l, targetcat) |
|
1356 else: |
|
1357 othertroubles = [] |
|
1358 for cat in unselectedcategories: |
|
1359 if troubled[cat]: |
|
1360 othertroubles.append(cat) |
|
1361 if othertroubles: |
|
1362 hint = hintmap['any+'+('+'.join(othertroubles))] |
|
1363 else: |
|
1364 msg = _("no troubled changesets") |
|
1365 |
|
1366 assert msg is not None |
|
1367 ui.write_err(msg+"\n") |
|
1368 if hint: |
|
1369 ui.write_err("("+hint+")\n") |
|
1370 return 2 |
|
1371 else: |
|
1372 return 1 |
|
1373 |
|
1374 def _cleanup(ui, repo, startnode, showprogress): |
|
1375 if showprogress: |
|
1376 ui.progress(_('evolve'), None) |
|
1377 if repo['.'] != startnode: |
|
1378 ui.status(_('working directory is now at %s\n') % repo['.']) |
|
1379 |
|
1380 class MultipleSuccessorsError(RuntimeError): |
|
1381 """Exception raised by _singlesuccessor when multiple successor sets exists |
|
1382 |
|
1383 The object contains the list of successorssets in its 'successorssets' |
|
1384 attribute to call to easily recover. |
|
1385 """ |
|
1386 |
|
1387 def __init__(self, successorssets): |
|
1388 self.successorssets = successorssets |
|
1389 |
|
1390 def _singlesuccessor(repo, p): |
|
1391 """returns p (as rev) if not obsolete or its unique latest successors |
|
1392 |
|
1393 fail if there are no such successor""" |
|
1394 |
|
1395 if not p.obsolete(): |
|
1396 return p.rev() |
|
1397 obs = repo[p] |
|
1398 ui = repo.ui |
|
1399 newer = obsolete.successorssets(repo, obs.node()) |
|
1400 # search of a parent which is not killed |
|
1401 while not newer: |
|
1402 ui.debug("stabilize target %s is plain dead," |
|
1403 " trying to stabilize on its parent\n" % |
|
1404 obs) |
|
1405 obs = obs.parents()[0] |
|
1406 newer = obsolete.successorssets(repo, obs.node()) |
|
1407 if len(newer) > 1 or len(newer[0]) > 1: |
|
1408 raise MultipleSuccessorsError(newer) |
|
1409 |
|
1410 return repo[newer[0][0]].rev() |
|
1411 |
|
1412 def builddependencies(repo, revs): |
|
1413 """returns dependency graphs giving an order to solve instability of revs |
|
1414 (see _orderrevs for more information on usage)""" |
|
1415 |
|
1416 # For each troubled revision we keep track of what instability if any should |
|
1417 # be resolved in order to resolve it. Example: |
|
1418 # dependencies = {3: [6], 6:[]} |
|
1419 # Means that: 6 has no dependency, 3 depends on 6 to be solved |
|
1420 dependencies = {} |
|
1421 # rdependencies is the inverted dict of dependencies |
|
1422 rdependencies = collections.defaultdict(set) |
|
1423 |
|
1424 for r in revs: |
|
1425 dependencies[r] = set() |
|
1426 for p in repo[r].parents(): |
|
1427 try: |
|
1428 succ = _singlesuccessor(repo, p) |
|
1429 except MultipleSuccessorsError as exc: |
|
1430 dependencies[r] = exc.successorssets |
|
1431 continue |
|
1432 if succ in revs: |
|
1433 dependencies[r].add(succ) |
|
1434 rdependencies[succ].add(r) |
|
1435 return dependencies, rdependencies |
|
1436 |
|
1437 def _dedupedivergents(repo, revs): |
|
1438 """Dedupe the divergents revs in revs to get one from each group with the |
|
1439 lowest revision numbers |
|
1440 """ |
|
1441 repo = repo.unfiltered() |
|
1442 res = set() |
|
1443 # To not reevaluate divergents of the same group once one is encountered |
|
1444 discarded = set() |
|
1445 for rev in revs: |
|
1446 if rev in discarded: |
|
1447 continue |
|
1448 divergent = repo[rev] |
|
1449 base, others = divergentdata(divergent) |
|
1450 othersrevs = [o.rev() for o in others] |
|
1451 res.add(min([divergent.rev()] + othersrevs)) |
|
1452 discarded.update(othersrevs) |
|
1453 return res |
|
1454 |
|
1455 def _selectrevs(repo, allopt, revopt, anyopt, targetcat): |
|
1456 """select troubles in repo matching according to given options""" |
|
1457 revs = set() |
|
1458 if allopt or revopt: |
|
1459 revs = repo.revs(targetcat+'()') |
|
1460 if revopt: |
|
1461 revs = scmutil.revrange(repo, revopt) & revs |
|
1462 elif not anyopt: |
|
1463 topic = getattr(repo, 'currenttopic', '') |
|
1464 if topic: |
|
1465 revs = repo.revs('topic(%s)', topic) & revs |
|
1466 elif targetcat == 'unstable': |
|
1467 revs = _aspiringdescendant(repo, |
|
1468 repo.revs('(.::) - obsolete()::')) |
|
1469 revs = set(revs) |
|
1470 if targetcat == 'divergent': |
|
1471 # Pick one divergent per group of divergents |
|
1472 revs = _dedupedivergents(repo, revs) |
|
1473 elif anyopt: |
|
1474 revs = repo.revs('first(%s())' % (targetcat)) |
|
1475 elif targetcat == 'unstable': |
|
1476 revs = set(_aspiringchildren(repo, repo.revs('(.::) - obsolete()::'))) |
|
1477 if 1 < len(revs): |
|
1478 msg = "multiple evolve candidates" |
|
1479 hint = (_("select one of %s with --rev") |
|
1480 % ', '.join([str(repo[r]) for r in sorted(revs)])) |
|
1481 raise error.Abort(msg, hint=hint) |
|
1482 elif targetcat in repo['.'].troubles(): |
|
1483 revs = set([repo['.'].rev()]) |
|
1484 return revs |
|
1485 |
|
1486 |
|
1487 def _orderrevs(repo, revs): |
|
1488 """Compute an ordering to solve instability for the given revs |
|
1489 |
|
1490 revs is a list of unstable revisions. |
|
1491 |
|
1492 Returns the same revisions ordered to solve their instability from the |
|
1493 bottom to the top of the stack that the stabilization process will produce |
|
1494 eventually. |
|
1495 |
|
1496 This ensures the minimal number of stabilizations, as we can stabilize each |
|
1497 revision on its final stabilized destination. |
|
1498 """ |
|
1499 # Step 1: Build the dependency graph |
|
1500 dependencies, rdependencies = builddependencies(repo, revs) |
|
1501 # Step 2: Build the ordering |
|
1502 # Remove the revisions with no dependency(A) and add them to the ordering. |
|
1503 # Removing these revisions leads to new revisions with no dependency (the |
|
1504 # one depending on A) that we can remove from the dependency graph and add |
|
1505 # to the ordering. We progress in a similar fashion until the ordering is |
|
1506 # built |
|
1507 solvablerevs = collections.deque([r for r in sorted(dependencies.keys()) |
|
1508 if not dependencies[r]]) |
|
1509 ordering = [] |
|
1510 while solvablerevs: |
|
1511 rev = solvablerevs.popleft() |
|
1512 for dependent in rdependencies[rev]: |
|
1513 dependencies[dependent].remove(rev) |
|
1514 if not dependencies[dependent]: |
|
1515 solvablerevs.append(dependent) |
|
1516 del dependencies[rev] |
|
1517 ordering.append(rev) |
|
1518 |
|
1519 ordering.extend(sorted(dependencies)) |
|
1520 return ordering |
|
1521 |
|
1522 def divergentsets(repo, ctx): |
|
1523 """Compute sets of commits divergent with a given one""" |
|
1524 cache = {} |
|
1525 succsets = {} |
|
1526 base = {} |
|
1527 for n in obsolete.allprecursors(repo.obsstore, [ctx.node()]): |
|
1528 if n == ctx.node(): |
|
1529 # a node can't be a base for divergence with itself |
|
1530 continue |
|
1531 nsuccsets = obsolete.successorssets(repo, n, cache) |
|
1532 for nsuccset in nsuccsets: |
|
1533 if ctx.node() in nsuccset: |
|
1534 # we are only interested in *other* successor sets |
|
1535 continue |
|
1536 if tuple(nsuccset) in base: |
|
1537 # we already know the latest base for this divergency |
|
1538 continue |
|
1539 base[tuple(nsuccset)] = n |
|
1540 divergence = [] |
|
1541 for divset, b in base.iteritems(): |
|
1542 divergence.append({ |
|
1543 'divergentnodes': divset, |
|
1544 'commonprecursor': b |
|
1545 }) |
|
1546 |
|
1547 return divergence |
|
1548 |
|
1549 def _preparelistctxs(items, condition): |
|
1550 return [item.hex() for item in items if condition(item)] |
|
1551 |
|
1552 def _formatctx(fm, ctx): |
|
1553 fm.data(node=ctx.hex()) |
|
1554 fm.data(desc=ctx.description()) |
|
1555 fm.data(date=ctx.date()) |
|
1556 fm.data(user=ctx.user()) |
|
1557 |
|
1558 def listtroubles(ui, repo, troublecategories, **opts): |
|
1559 """Print all the troubles for the repo (or given revset)""" |
|
1560 troublecategories = troublecategories or ['divergent', 'unstable', 'bumped'] |
|
1561 showunstable = 'unstable' in troublecategories |
|
1562 showbumped = 'bumped' in troublecategories |
|
1563 showdivergent = 'divergent' in troublecategories |
|
1564 |
|
1565 revs = repo.revs('+'.join("%s()" % t for t in troublecategories)) |
|
1566 if opts.get('rev'): |
|
1567 revs = revs & repo.revs(opts.get('rev')) |
|
1568 |
|
1569 fm = ui.formatter('evolvelist', opts) |
|
1570 for rev in revs: |
|
1571 ctx = repo[rev] |
|
1572 unpars = _preparelistctxs(ctx.parents(), lambda p: p.unstable()) |
|
1573 obspars = _preparelistctxs(ctx.parents(), lambda p: p.obsolete()) |
|
1574 imprecs = _preparelistctxs(repo.set("allprecursors(%n)", ctx.node()), |
|
1575 lambda p: not p.mutable()) |
|
1576 dsets = divergentsets(repo, ctx) |
|
1577 |
|
1578 fm.startitem() |
|
1579 # plain formatter section |
|
1580 hashlen, desclen = 12, 60 |
|
1581 desc = ctx.description() |
|
1582 if desc: |
|
1583 desc = desc.splitlines()[0] |
|
1584 desc = (desc[:desclen] + '...') if len(desc) > desclen else desc |
|
1585 fm.plain('%s: ' % ctx.hex()[:hashlen]) |
|
1586 fm.plain('%s\n' % desc) |
|
1587 fm.data(node=ctx.hex(), rev=ctx.rev(), desc=desc, phase=ctx.phasestr()) |
|
1588 |
|
1589 for unpar in unpars if showunstable else []: |
|
1590 fm.plain(' unstable: %s (unstable parent)\n' % unpar[:hashlen]) |
|
1591 for obspar in obspars if showunstable else []: |
|
1592 fm.plain(' unstable: %s (obsolete parent)\n' % obspar[:hashlen]) |
|
1593 for imprec in imprecs if showbumped else []: |
|
1594 fm.plain(' bumped: %s (immutable precursor)\n' % imprec[:hashlen]) |
|
1595 |
|
1596 if dsets and showdivergent: |
|
1597 for dset in dsets: |
|
1598 fm.plain(' divergent: ') |
|
1599 first = True |
|
1600 for n in dset['divergentnodes']: |
|
1601 t = "%s (%s)" if first else " %s (%s)" |
|
1602 first = False |
|
1603 fm.plain(t % (node.hex(n)[:hashlen], repo[n].phasestr())) |
|
1604 comprec = node.hex(dset['commonprecursor'])[:hashlen] |
|
1605 fm.plain(" (precursor %s)\n" % comprec) |
|
1606 fm.plain("\n") |
|
1607 |
|
1608 # templater-friendly section |
|
1609 _formatctx(fm, ctx) |
|
1610 troubles = [] |
|
1611 for unpar in unpars: |
|
1612 troubles.append({'troubletype': 'unstable', 'sourcenode': unpar, |
|
1613 'sourcetype': 'unstableparent'}) |
|
1614 for obspar in obspars: |
|
1615 troubles.append({'troubletype': 'unstable', 'sourcenode': obspar, |
|
1616 'sourcetype': 'obsoleteparent'}) |
|
1617 for imprec in imprecs: |
|
1618 troubles.append({'troubletype': 'bumped', 'sourcenode': imprec, |
|
1619 'sourcetype': 'immutableprecursor'}) |
|
1620 for dset in dsets: |
|
1621 divnodes = [{'node': node.hex(n), |
|
1622 'phase': repo[n].phasestr(), |
|
1623 } for n in dset['divergentnodes']] |
|
1624 troubles.append({'troubletype': 'divergent', |
|
1625 'commonprecursor': node.hex(dset['commonprecursor']), |
|
1626 'divergentnodes': divnodes}) |
|
1627 fm.data(troubles=troubles) |
|
1628 |
|
1629 fm.end() |
|
1630 |
|
1631 @command('^evolve|stabilize|solve', |
|
1632 [('n', 'dry-run', False, |
|
1633 _('do not perform actions, just print what would be done')), |
|
1634 ('', 'confirm', False, |
|
1635 _('ask for confirmation before performing the action')), |
|
1636 ('A', 'any', False, |
|
1637 _('also consider troubled changesets unrelated to current working ' |
|
1638 'directory')), |
|
1639 ('r', 'rev', [], _('solves troubles of these revisions')), |
|
1640 ('', 'bumped', False, _('solves only bumped changesets')), |
|
1641 ('', 'divergent', False, _('solves only divergent changesets')), |
|
1642 ('', 'unstable', False, _('solves only unstable changesets (default)')), |
|
1643 ('a', 'all', False, _('evolve all troubled changesets related to the ' |
|
1644 'current working directory and its descendants')), |
|
1645 ('c', 'continue', False, _('continue an interrupted evolution')), |
|
1646 ('l', 'list', False, 'provide details on troubled changesets in the repo'), |
|
1647 ] + mergetoolopts, |
|
1648 _('[OPTIONS]...')) |
|
1649 def evolve(ui, repo, **opts): |
|
1650 """solve troubled changesets in your repository |
|
1651 |
|
1652 Modifying history can lead to various types of troubled changesets: |
|
1653 unstable, bumped, or divergent. The evolve command resolves your troubles |
|
1654 by executing one of the following actions: |
|
1655 |
|
1656 - update working copy to a successor |
|
1657 - rebase an unstable changeset |
|
1658 - extract the desired changes from a bumped changeset |
|
1659 - fuse divergent changesets back together |
|
1660 |
|
1661 If you pass no arguments, evolve works in automatic mode: it will execute a |
|
1662 single action to reduce instability related to your working copy. There are |
|
1663 two cases for this action. First, if the parent of your working copy is |
|
1664 obsolete, evolve updates to the parent's successor. Second, if the working |
|
1665 copy parent is not obsolete but has obsolete predecessors, then evolve |
|
1666 determines if there is an unstable changeset that can be rebased onto the |
|
1667 working copy parent in order to reduce instability. |
|
1668 If so, evolve rebases that changeset. If not, evolve refuses to guess your |
|
1669 intention, and gives a hint about what you might want to do next. |
|
1670 |
|
1671 Any time evolve creates a changeset, it updates the working copy to the new |
|
1672 changeset. (Currently, every successful evolve operation involves an update |
|
1673 as well; this may change in future.) |
|
1674 |
|
1675 Automatic mode only handles common use cases. For example, it avoids taking |
|
1676 action in the case of ambiguity, and it ignores unstable changesets that |
|
1677 are not related to your working copy. |
|
1678 It also refuses to solve bumped or divergent changesets unless you explicity |
|
1679 request such behavior (see below). |
|
1680 |
|
1681 Eliminating all instability around your working copy may require multiple |
|
1682 invocations of :hg:`evolve`. Alternately, use ``--all`` to recursively |
|
1683 select and evolve all unstable changesets that can be rebased onto the |
|
1684 working copy parent. |
|
1685 This is more powerful than successive invocations, since ``--all`` handles |
|
1686 ambiguous cases (e.g. unstable changesets with multiple children) by |
|
1687 evolving all branches. |
|
1688 |
|
1689 When your repository cannot be handled by automatic mode, you might need to |
|
1690 use ``--rev`` to specify a changeset to evolve. For example, if you have |
|
1691 an unstable changeset that is not related to the working copy parent, |
|
1692 you could use ``--rev`` to evolve it. Or, if some changeset has multiple |
|
1693 unstable children, evolve in automatic mode refuses to guess which one to |
|
1694 evolve; you have to use ``--rev`` in that case. |
|
1695 |
|
1696 Alternately, ``--any`` makes evolve search for the next evolvable changeset |
|
1697 regardless of whether it is related to the working copy parent. |
|
1698 |
|
1699 You can supply multiple revisions to evolve multiple troubled changesets |
|
1700 in a single invocation. In revset terms, ``--any`` is equivalent to ``--rev |
|
1701 first(unstable())``. ``--rev`` and ``--all`` are mutually exclusive, as are |
|
1702 ``--rev`` and ``--any``. |
|
1703 |
|
1704 ``hg evolve --any --all`` is useful for cleaning up instability across all |
|
1705 branches, letting evolve figure out the appropriate order and destination. |
|
1706 |
|
1707 When you have troubled changesets that are not unstable, :hg:`evolve` |
|
1708 refuses to consider them unless you specify the category of trouble you |
|
1709 wish to resolve, with ``--bumped`` or ``--divergent``. These options are |
|
1710 currently mutually exclusive with each other and with ``--unstable`` |
|
1711 (the default). You can combine ``--bumped`` or ``--divergent`` with |
|
1712 ``--rev``, ``--all``, or ``--any``. |
|
1713 |
|
1714 You can also use the evolve command to list the troubles affecting your |
|
1715 repository by using the --list flag. You can choose to display only some |
|
1716 categories of troubles with the --unstable, --divergent or --bumped flags. |
|
1717 """ |
|
1718 |
|
1719 # Options |
|
1720 listopt = opts['list'] |
|
1721 contopt = opts['continue'] |
|
1722 anyopt = opts['any'] |
|
1723 allopt = opts['all'] |
|
1724 startnode = repo['.'] |
|
1725 dryrunopt = opts['dry_run'] |
|
1726 confirmopt = opts['confirm'] |
|
1727 revopt = opts['rev'] |
|
1728 troublecategories = ['bumped', 'divergent', 'unstable'] |
|
1729 specifiedcategories = [t for t in troublecategories if opts[t]] |
|
1730 if listopt: |
|
1731 listtroubles(ui, repo, specifiedcategories, **opts) |
|
1732 return |
|
1733 |
|
1734 targetcat = 'unstable' |
|
1735 if 1 < len(specifiedcategories): |
|
1736 msg = _('cannot specify more than one trouble category to solve (yet)') |
|
1737 raise error.Abort(msg) |
|
1738 elif len(specifiedcategories) == 1: |
|
1739 targetcat = specifiedcategories[0] |
|
1740 elif repo['.'].obsolete(): |
|
1741 displayer = cmdutil.show_changeset(ui, repo, |
|
1742 {'template': shorttemplate}) |
|
1743 # no args and parent is obsolete, update to successors |
|
1744 try: |
|
1745 ctx = repo[_singlesuccessor(repo, repo['.'])] |
|
1746 except MultipleSuccessorsError as exc: |
|
1747 repo.ui.write_err('parent is obsolete with multiple successors:\n') |
|
1748 for ln in exc.successorssets: |
|
1749 for n in ln: |
|
1750 displayer.show(repo[n]) |
|
1751 return 2 |
|
1752 |
|
1753 |
|
1754 ui.status(_('update:')) |
|
1755 if not ui.quiet: |
|
1756 displayer.show(ctx) |
|
1757 |
|
1758 if dryrunopt: |
|
1759 return 0 |
|
1760 res = hg.update(repo, ctx.rev()) |
|
1761 if ctx != startnode: |
|
1762 ui.status(_('working directory is now at %s\n') % ctx) |
|
1763 return res |
|
1764 |
|
1765 ui.setconfig('ui', 'forcemerge', opts.get('tool', ''), 'evolve') |
|
1766 troubled = set(repo.revs('troubled()')) |
|
1767 |
|
1768 # Progress handling |
|
1769 seen = 1 |
|
1770 count = allopt and len(troubled) or 1 |
|
1771 showprogress = allopt |
|
1772 |
|
1773 def progresscb(): |
|
1774 if revopt or allopt: |
|
1775 ui.progress(_('evolve'), seen, unit=_('changesets'), total=count) |
|
1776 |
|
1777 # Continuation handling |
|
1778 if contopt: |
|
1779 if anyopt: |
|
1780 raise error.Abort('cannot specify both "--any" and "--continue"') |
|
1781 if allopt: |
|
1782 raise error.Abort('cannot specify both "--all" and "--continue"') |
|
1783 state = _evolvestateread(repo) |
|
1784 if state is None: |
|
1785 raise error.Abort('no evolve to continue') |
|
1786 orig = repo[state['current']] |
|
1787 # XXX This is a terrible terrible hack, please get rid of it. |
|
1788 lock = repo.wlock() |
|
1789 try: |
|
1790 repo.opener.write('graftstate', orig.hex() + '\n') |
|
1791 try: |
|
1792 graftcmd = commands.table['graft'][0] |
|
1793 ret = graftcmd(ui, repo, old_obsolete=True, **{'continue': True}) |
|
1794 _evolvestatedelete(repo) |
|
1795 return ret |
|
1796 finally: |
|
1797 util.unlinkpath(repo.join('graftstate'), ignoremissing=True) |
|
1798 finally: |
|
1799 lock.release() |
|
1800 cmdutil.bailifchanged(repo) |
|
1801 |
|
1802 |
|
1803 if revopt and allopt: |
|
1804 raise error.Abort('cannot specify both "--rev" and "--all"') |
|
1805 if revopt and anyopt: |
|
1806 raise error.Abort('cannot specify both "--rev" and "--any"') |
|
1807 |
|
1808 revs = _selectrevs(repo, allopt, revopt, anyopt, targetcat) |
|
1809 |
|
1810 if not revs: |
|
1811 return _handlenotrouble(ui, repo, allopt, revopt, anyopt, targetcat) |
|
1812 |
|
1813 # For the progress bar to show |
|
1814 count = len(revs) |
|
1815 # Order the revisions |
|
1816 if targetcat == 'unstable': |
|
1817 revs = _orderrevs(repo, revs) |
|
1818 for rev in revs: |
|
1819 progresscb() |
|
1820 _solveone(ui, repo, repo[rev], dryrunopt, confirmopt, |
|
1821 progresscb, targetcat) |
|
1822 seen += 1 |
|
1823 progresscb() |
|
1824 _cleanup(ui, repo, startnode, showprogress) |
|
1825 |
|
1826 def _possibledestination(repo, rev): |
|
1827 """return all changesets that may be a new parent for REV""" |
|
1828 tonode = repo.changelog.node |
|
1829 parents = repo.changelog.parentrevs |
|
1830 torev = repo.changelog.rev |
|
1831 dest = set() |
|
1832 tovisit = list(parents(rev)) |
|
1833 while tovisit: |
|
1834 r = tovisit.pop() |
|
1835 succsets = obsolete.successorssets(repo, tonode(r)) |
|
1836 if not succsets: |
|
1837 tovisit.extend(parents(r)) |
|
1838 else: |
|
1839 # We should probably pick only one destination from split |
|
1840 # (case where '1 < len(ss)'), This could be the currently tipmost |
|
1841 # but logic is less clear when result of the split are now on |
|
1842 # multiple branches. |
|
1843 for ss in succsets: |
|
1844 for n in ss: |
|
1845 dest.add(torev(n)) |
|
1846 return dest |
|
1847 |
|
1848 def _aspiringchildren(repo, revs): |
|
1849 """Return a list of changectx which can be stabilized on top of pctx or |
|
1850 one of its descendants. Empty list if none can be found.""" |
|
1851 target = set(revs) |
|
1852 result = [] |
|
1853 for r in repo.revs('unstable() - %ld', revs): |
|
1854 dest = _possibledestination(repo, r) |
|
1855 if target & dest: |
|
1856 result.append(r) |
|
1857 return result |
|
1858 |
|
1859 def _aspiringdescendant(repo, revs): |
|
1860 """Return a list of changectx which can be stabilized on top of pctx or |
|
1861 one of its descendants recursively. Empty list if none can be found.""" |
|
1862 target = set(revs) |
|
1863 result = set(target) |
|
1864 paths = collections.defaultdict(set) |
|
1865 for r in repo.revs('unstable() - %ld', revs): |
|
1866 for d in _possibledestination(repo, r): |
|
1867 paths[d].add(r) |
|
1868 |
|
1869 result = set(target) |
|
1870 tovisit = list(revs) |
|
1871 while tovisit: |
|
1872 base = tovisit.pop() |
|
1873 for unstable in paths[base]: |
|
1874 if unstable not in result: |
|
1875 tovisit.append(unstable) |
|
1876 result.add(unstable) |
|
1877 return sorted(result - target) |
|
1878 |
|
1879 def _solveunstable(ui, repo, orig, dryrun=False, confirm=False, |
|
1880 progresscb=None): |
|
1881 """Stabilize an unstable changeset""" |
|
1882 pctx = orig.p1() |
|
1883 if len(orig.parents()) == 2: |
|
1884 if not pctx.obsolete(): |
|
1885 pctx = orig.p2() # second parent is obsolete ? |
|
1886 elif orig.p2().obsolete(): |
|
1887 hint = _("Redo the merge (%s) and use `hg prune <old> " |
|
1888 "--succ <new>` to obsolete the old one") % orig.hex()[:12] |
|
1889 ui.warn(_("warning: no support for evolving merge changesets " |
|
1890 "with two obsolete parents yet\n") + |
|
1891 _("(%s)\n") % hint) |
|
1892 return False |
|
1893 |
|
1894 if not pctx.obsolete(): |
|
1895 ui.warn(_("cannot solve instability of %s, skipping\n") % orig) |
|
1896 return False |
|
1897 obs = pctx |
|
1898 newer = obsolete.successorssets(repo, obs.node()) |
|
1899 # search of a parent which is not killed |
|
1900 while not newer or newer == [()]: |
|
1901 ui.debug("stabilize target %s is plain dead," |
|
1902 " trying to stabilize on its parent\n" % |
|
1903 obs) |
|
1904 obs = obs.parents()[0] |
|
1905 newer = obsolete.successorssets(repo, obs.node()) |
|
1906 if len(newer) > 1: |
|
1907 msg = _("skipping %s: divergent rewriting. can't choose " |
|
1908 "destination\n") % obs |
|
1909 ui.write_err(msg) |
|
1910 return 2 |
|
1911 targets = newer[0] |
|
1912 assert targets |
|
1913 if len(targets) > 1: |
|
1914 # split target, figure out which one to pick, are they all in line? |
|
1915 targetrevs = [repo[r].rev() for r in targets] |
|
1916 roots = repo.revs('roots(%ld)', targetrevs) |
|
1917 heads = repo.revs('heads(%ld)', targetrevs) |
|
1918 if len(roots) > 1 or len(heads) > 1: |
|
1919 msg = "cannot solve split accross two branches\n" |
|
1920 ui.write_err(msg) |
|
1921 return 2 |
|
1922 target = repo[heads.first()] |
|
1923 else: |
|
1924 target = targets[0] |
|
1925 displayer = cmdutil.show_changeset(ui, repo, {'template': shorttemplate}) |
|
1926 target = repo[target] |
|
1927 if not ui.quiet or confirm: |
|
1928 repo.ui.write(_('move:')) |
|
1929 displayer.show(orig) |
|
1930 repo.ui.write(_('atop:')) |
|
1931 displayer.show(target) |
|
1932 if confirm and ui.prompt('perform evolve? [Ny]', 'n') != 'y': |
|
1933 raise error.Abort(_('evolve aborted by user')) |
|
1934 if progresscb: progresscb() |
|
1935 todo = 'hg rebase -r %s -d %s\n' % (orig, target) |
|
1936 if dryrun: |
|
1937 repo.ui.write(todo) |
|
1938 else: |
|
1939 repo.ui.note(todo) |
|
1940 if progresscb: progresscb() |
|
1941 keepbranch = orig.p1().branch() != orig.branch() |
|
1942 try: |
|
1943 relocate(repo, orig, target, pctx, keepbranch) |
|
1944 except MergeFailure: |
|
1945 _evolvestatewrite(repo, {'current': orig.node()}) |
|
1946 repo.ui.write_err(_('evolve failed!\n')) |
|
1947 repo.ui.write_err( |
|
1948 _("fix conflict and run 'hg evolve --continue'" |
|
1949 " or use 'hg update -C .' to abort\n")) |
|
1950 raise |
|
1951 |
|
1952 def _solvebumped(ui, repo, bumped, dryrun=False, confirm=False, |
|
1953 progresscb=None): |
|
1954 """Stabilize a bumped changeset""" |
|
1955 repo = repo.unfiltered() |
|
1956 bumped = repo[bumped.rev()] |
|
1957 # For now we deny bumped merge |
|
1958 if len(bumped.parents()) > 1: |
|
1959 msg = _('skipping %s : we do not handle merge yet\n') % bumped |
|
1960 ui.write_err(msg) |
|
1961 return 2 |
|
1962 prec = repo.set('last(allprecursors(%d) and public())', bumped).next() |
|
1963 # For now we deny target merge |
|
1964 if len(prec.parents()) > 1: |
|
1965 msg = _('skipping: %s: public version is a merge, ' |
|
1966 'this is not handled yet\n') % prec |
|
1967 ui.write_err(msg) |
|
1968 return 2 |
|
1969 |
|
1970 displayer = cmdutil.show_changeset(ui, repo, {'template': shorttemplate}) |
|
1971 if not ui.quiet or confirm: |
|
1972 repo.ui.write(_('recreate:')) |
|
1973 displayer.show(bumped) |
|
1974 repo.ui.write(_('atop:')) |
|
1975 displayer.show(prec) |
|
1976 if confirm and ui.prompt('perform evolve? [Ny]', 'n') != 'y': |
|
1977 raise error.Abort(_('evolve aborted by user')) |
|
1978 if dryrun: |
|
1979 todo = 'hg rebase --rev %s --dest %s;\n' % (bumped, prec.p1()) |
|
1980 repo.ui.write(todo) |
|
1981 repo.ui.write(('hg update %s;\n' % prec)) |
|
1982 repo.ui.write(('hg revert --all --rev %s;\n' % bumped)) |
|
1983 repo.ui.write(('hg commit --msg "bumped update to %s"')) |
|
1984 return 0 |
|
1985 if progresscb: progresscb() |
|
1986 newid = tmpctx = None |
|
1987 tmpctx = bumped |
|
1988 # Basic check for common parent. Far too complicated and fragile |
|
1989 tr = repo.currenttransaction() |
|
1990 assert tr is not None |
|
1991 bmupdate = _bookmarksupdater(repo, bumped.node(), tr) |
|
1992 if not list(repo.set('parents(%d) and parents(%d)', bumped, prec)): |
|
1993 # Need to rebase the changeset at the right place |
|
1994 repo.ui.status( |
|
1995 _('rebasing to destination parent: %s\n') % prec.p1()) |
|
1996 try: |
|
1997 tmpid = relocate(repo, bumped, prec.p1()) |
|
1998 if tmpid is not None: |
|
1999 tmpctx = repo[tmpid] |
|
2000 obsolete.createmarkers(repo, [(bumped, (tmpctx,))]) |
|
2001 except MergeFailure: |
|
2002 repo.opener.write('graftstate', bumped.hex() + '\n') |
|
2003 repo.ui.write_err(_('evolution failed!\n')) |
|
2004 repo.ui.write_err( |
|
2005 _("fix conflict and run 'hg evolve --continue'\n")) |
|
2006 raise |
|
2007 # Create the new commit context |
|
2008 repo.ui.status(_('computing new diff\n')) |
|
2009 files = set() |
|
2010 copied = copies.pathcopies(prec, bumped) |
|
2011 precmanifest = prec.manifest().copy() |
|
2012 # 3.3.2 needs a list. |
|
2013 # future 3.4 don't detect the size change during iteration |
|
2014 # this is fishy |
|
2015 for key, val in list(bumped.manifest().iteritems()): |
|
2016 precvalue = precmanifest.get(key, None) |
|
2017 if precvalue is not None: |
|
2018 del precmanifest[key] |
|
2019 if precvalue != val: |
|
2020 files.add(key) |
|
2021 files.update(precmanifest) # add missing files |
|
2022 # commit it |
|
2023 if files: # something to commit! |
|
2024 def filectxfn(repo, ctx, path): |
|
2025 if path in bumped: |
|
2026 fctx = bumped[path] |
|
2027 flags = fctx.flags() |
|
2028 mctx = memfilectx(repo, fctx.path(), fctx.data(), |
|
2029 islink='l' in flags, |
|
2030 isexec='x' in flags, |
|
2031 copied=copied.get(path)) |
|
2032 return mctx |
|
2033 return None |
|
2034 text = 'bumped update to %s:\n\n' % prec |
|
2035 text += bumped.description() |
|
2036 |
|
2037 new = context.memctx(repo, |
|
2038 parents=[prec.node(), node.nullid], |
|
2039 text=text, |
|
2040 files=files, |
|
2041 filectxfn=filectxfn, |
|
2042 user=bumped.user(), |
|
2043 date=bumped.date(), |
|
2044 extra=bumped.extra()) |
|
2045 |
|
2046 newid = repo.commitctx(new) |
|
2047 if newid is None: |
|
2048 obsolete.createmarkers(repo, [(tmpctx, ())]) |
|
2049 newid = prec.node() |
|
2050 else: |
|
2051 phases.retractboundary(repo, tr, bumped.phase(), [newid]) |
|
2052 obsolete.createmarkers(repo, [(tmpctx, (repo[newid],))], |
|
2053 flag=obsolete.bumpedfix) |
|
2054 bmupdate(newid) |
|
2055 repo.ui.status(_('committed as %s\n') % node.short(newid)) |
|
2056 # reroute the working copy parent to the new changeset |
|
2057 repo.dirstate.beginparentchange() |
|
2058 repo.dirstate.setparents(newid, node.nullid) |
|
2059 repo.dirstate.endparentchange() |
|
2060 |
|
2061 def _solvedivergent(ui, repo, divergent, dryrun=False, confirm=False, |
|
2062 progresscb=None): |
|
2063 repo = repo.unfiltered() |
|
2064 divergent = repo[divergent.rev()] |
|
2065 base, others = divergentdata(divergent) |
|
2066 if len(others) > 1: |
|
2067 othersstr = "[%s]" % (','.join([str(i) for i in others])) |
|
2068 msg = _("skipping %d:divergent with a changeset that got splitted" |
|
2069 " into multiple ones:\n" |
|
2070 "|[%s]\n" |
|
2071 "| This is not handled by automatic evolution yet\n" |
|
2072 "| You have to fallback to manual handling with commands " |
|
2073 "such as:\n" |
|
2074 "| - hg touch -D\n" |
|
2075 "| - hg prune\n" |
|
2076 "| \n" |
|
2077 "| You should contact your local evolution Guru for help.\n" |
|
2078 ) % (divergent, othersstr) |
|
2079 ui.write_err(msg) |
|
2080 return 2 |
|
2081 other = others[0] |
|
2082 if len(other.parents()) > 1: |
|
2083 msg = _("skipping %s: divergent changeset can't be " |
|
2084 "a merge (yet)\n") % divergent |
|
2085 ui.write_err(msg) |
|
2086 hint = _("You have to fallback to solving this by hand...\n" |
|
2087 "| This probably means redoing the merge and using \n" |
|
2088 "| `hg prune` to kill older version.\n") |
|
2089 ui.write_err(hint) |
|
2090 return 2 |
|
2091 if other.p1() not in divergent.parents(): |
|
2092 msg = _("skipping %s: have a different parent than %s " |
|
2093 "(not handled yet)\n") % (divergent, other) |
|
2094 hint = _("| %(d)s, %(o)s are not based on the same changeset.\n" |
|
2095 "| With the current state of its implementation, \n" |
|
2096 "| evolve does not work in that case.\n" |
|
2097 "| rebase one of them next to the other and run \n" |
|
2098 "| this command again.\n" |
|
2099 "| - either: hg rebase --dest 'p1(%(d)s)' -r %(o)s\n" |
|
2100 "| - or: hg rebase --dest 'p1(%(o)s)' -r %(d)s\n" |
|
2101 ) % {'d': divergent, 'o': other} |
|
2102 ui.write_err(msg) |
|
2103 ui.write_err(hint) |
|
2104 return 2 |
|
2105 |
|
2106 displayer = cmdutil.show_changeset(ui, repo, {'template': shorttemplate}) |
|
2107 if not ui.quiet or confirm: |
|
2108 ui.write(_('merge:')) |
|
2109 displayer.show(divergent) |
|
2110 ui.write(_('with: ')) |
|
2111 displayer.show(other) |
|
2112 ui.write(_('base: ')) |
|
2113 displayer.show(base) |
|
2114 if confirm and ui.prompt(_('perform evolve? [Ny]'), 'n') != 'y': |
|
2115 raise error.Abort(_('evolve aborted by user')) |
|
2116 if dryrun: |
|
2117 ui.write(('hg update -c %s &&\n' % divergent)) |
|
2118 ui.write(('hg merge %s &&\n' % other)) |
|
2119 ui.write(('hg commit -m "auto merge resolving conflict between ' |
|
2120 '%s and %s"&&\n' % (divergent, other))) |
|
2121 ui.write(('hg up -C %s &&\n' % base)) |
|
2122 ui.write(('hg revert --all --rev tip &&\n')) |
|
2123 ui.write(('hg commit -m "`hg log -r %s --template={desc}`";\n' |
|
2124 % divergent)) |
|
2125 return |
|
2126 if divergent not in repo[None].parents(): |
|
2127 repo.ui.status(_('updating to "local" conflict\n')) |
|
2128 hg.update(repo, divergent.rev()) |
|
2129 repo.ui.note(_('merging divergent changeset\n')) |
|
2130 if progresscb: progresscb() |
|
2131 try: |
|
2132 stats = merge.update(repo, |
|
2133 other.node(), |
|
2134 branchmerge=True, |
|
2135 force=False, |
|
2136 ancestor=base.node(), |
|
2137 mergeancestor=True) |
|
2138 except TypeError: |
|
2139 # Mercurial < 43c00ca887d1 (3.7) |
|
2140 stats = merge.update(repo, |
|
2141 other.node(), |
|
2142 branchmerge=True, |
|
2143 force=False, |
|
2144 partial=None, |
|
2145 ancestor=base.node(), |
|
2146 mergeancestor=True) |
|
2147 |
|
2148 hg._showstats(repo, stats) |
|
2149 if stats[3]: |
|
2150 repo.ui.status(_("use 'hg resolve' to retry unresolved file merges " |
|
2151 "or 'hg update -C .' to abort\n")) |
|
2152 if stats[3] > 0: |
|
2153 raise error.Abort('merge conflict between several amendments ' |
|
2154 '(this is not automated yet)', |
|
2155 hint="""/!\ You can try: |
|
2156 /!\ * manual merge + resolve => new cset X |
|
2157 /!\ * hg up to the parent of the amended changeset (which are named W and Z) |
|
2158 /!\ * hg revert --all -r X |
|
2159 /!\ * hg ci -m "same message as the amended changeset" => new cset Y |
|
2160 /!\ * hg prune -n Y W Z |
|
2161 """) |
|
2162 if progresscb: progresscb() |
|
2163 emtpycommitallowed = repo.ui.backupconfig('ui', 'allowemptycommit') |
|
2164 tr = repo.currenttransaction() |
|
2165 assert tr is not None |
|
2166 try: |
|
2167 repo.ui.setconfig('ui', 'allowemptycommit', True, 'evolve') |
|
2168 repo.dirstate.beginparentchange() |
|
2169 repo.dirstate.setparents(divergent.node(), node.nullid) |
|
2170 repo.dirstate.endparentchange() |
|
2171 oldlen = len(repo) |
|
2172 amend(ui, repo, message='', logfile='') |
|
2173 if oldlen == len(repo): |
|
2174 new = divergent |
|
2175 # no changes |
|
2176 else: |
|
2177 new = repo['.'] |
|
2178 obsolete.createmarkers(repo, [(other, (new,))]) |
|
2179 phases.retractboundary(repo, tr, other.phase(), [new.node()]) |
|
2180 finally: |
|
2181 repo.ui.restoreconfig(emtpycommitallowed) |
|
2182 |
|
2183 def divergentdata(ctx): |
|
2184 """return base, other part of a conflict |
|
2185 |
|
2186 This only return the first one. |
|
2187 |
|
2188 XXX this woobly function won't survive XXX |
|
2189 """ |
|
2190 repo = ctx._repo.unfiltered() |
|
2191 for base in repo.set('reverse(allprecursors(%d))', ctx): |
|
2192 newer = obsolete.successorssets(ctx._repo, base.node()) |
|
2193 # drop filter and solution including the original ctx |
|
2194 newer = [n for n in newer if n and ctx.node() not in n] |
|
2195 if newer: |
|
2196 return base, tuple(ctx._repo[o] for o in newer[0]) |
|
2197 raise error.Abort("base of divergent changeset %s not found" % ctx, |
|
2198 hint='this case is not yet handled') |
|
2199 |
|
2200 |
|
2201 |
|
2202 shorttemplate = '[{rev}] {desc|firstline}\n' |
|
2203 |
|
2204 @command('^previous', |
|
2205 [('B', 'move-bookmark', False, |
|
2206 _('move active bookmark after update')), |
|
2207 ('', 'merge', False, _('bring uncommitted change along')), |
|
2208 ('', 'no-topic', False, _('ignore topic and move topologically')), |
|
2209 ('n', 'dry-run', False, |
|
2210 _('do not perform actions, just print what would be done'))], |
|
2211 '[OPTION]...') |
|
2212 def cmdprevious(ui, repo, **opts): |
|
2213 """update to parent revision |
|
2214 |
|
2215 Displays the summary line of the destination for clarity.""" |
|
2216 wlock = None |
|
2217 dryrunopt = opts['dry_run'] |
|
2218 if not dryrunopt: |
|
2219 wlock = repo.wlock() |
|
2220 try: |
|
2221 wkctx = repo[None] |
|
2222 wparents = wkctx.parents() |
|
2223 if len(wparents) != 1: |
|
2224 raise error.Abort('merge in progress') |
|
2225 if not opts['merge']: |
|
2226 try: |
|
2227 cmdutil.bailifchanged(repo) |
|
2228 except error.Abort as exc: |
|
2229 exc.hint = _('do you want --merge?') |
|
2230 raise |
|
2231 |
|
2232 parents = wparents[0].parents() |
|
2233 topic = getattr(repo, 'currenttopic', '') |
|
2234 if topic and not opts.get("no_topic", False): |
|
2235 parents = [ctx for ctx in parents if ctx.topic() == topic] |
|
2236 displayer = cmdutil.show_changeset(ui, repo, {'template': shorttemplate}) |
|
2237 if not parents: |
|
2238 ui.warn(_('no parent in topic "%s"\n') % topic) |
|
2239 ui.warn(_('(do you want --no-topic)\n')) |
|
2240 elif len(parents) == 1: |
|
2241 p = parents[0] |
|
2242 bm = bmactive(repo) |
|
2243 shouldmove = opts.get('move_bookmark') and bm is not None |
|
2244 if dryrunopt: |
|
2245 ui.write(('hg update %s;\n' % p.rev())) |
|
2246 if shouldmove: |
|
2247 ui.write(('hg bookmark %s -r %s;\n' % (bm, p.rev()))) |
|
2248 else: |
|
2249 ret = hg.update(repo, p.rev()) |
|
2250 if not ret: |
|
2251 tr = lock = None |
|
2252 try: |
|
2253 lock = repo.lock() |
|
2254 tr = repo.transaction('previous') |
|
2255 if shouldmove: |
|
2256 repo._bookmarks[bm] = p.node() |
|
2257 repo._bookmarks.recordchange(tr) |
|
2258 else: |
|
2259 bmdeactivate(repo) |
|
2260 tr.close() |
|
2261 finally: |
|
2262 lockmod.release(tr, lock) |
|
2263 |
|
2264 displayer.show(p) |
|
2265 return 0 |
|
2266 else: |
|
2267 for p in parents: |
|
2268 displayer.show(p) |
|
2269 ui.warn(_('multiple parents, explicitly update to one\n')) |
|
2270 return 1 |
|
2271 finally: |
|
2272 lockmod.release(wlock) |
|
2273 |
|
2274 @command('^next', |
|
2275 [('B', 'move-bookmark', False, |
|
2276 _('move active bookmark after update')), |
|
2277 ('', 'merge', False, _('bring uncommitted change along')), |
|
2278 ('', 'evolve', False, _('evolve the next changeset if necessary')), |
|
2279 ('', 'no-topic', False, _('ignore topic and move topologically')), |
|
2280 ('n', 'dry-run', False, |
|
2281 _('do not perform actions, just print what would be done'))], |
|
2282 '[OPTION]...') |
|
2283 def cmdnext(ui, repo, **opts): |
|
2284 """update to next child revision |
|
2285 |
|
2286 Use the ``--evolve`` flag to evolve unstable children on demand. |
|
2287 |
|
2288 Displays the summary line of the destination for clarity. |
|
2289 """ |
|
2290 wlock = None |
|
2291 dryrunopt = opts['dry_run'] |
|
2292 if not dryrunopt: |
|
2293 wlock = repo.wlock() |
|
2294 try: |
|
2295 wkctx = repo[None] |
|
2296 wparents = wkctx.parents() |
|
2297 if len(wparents) != 1: |
|
2298 raise error.Abort('merge in progress') |
|
2299 if not opts['merge']: |
|
2300 try: |
|
2301 cmdutil.bailifchanged(repo) |
|
2302 except error.Abort as exc: |
|
2303 exc.hint = _('do you want --merge?') |
|
2304 raise |
|
2305 |
|
2306 children = [ctx for ctx in wparents[0].children() if not ctx.obsolete()] |
|
2307 topic = getattr(repo, 'currenttopic', '') |
|
2308 filtered = [] |
|
2309 if topic and not opts.get("no_topic", False): |
|
2310 filtered = [ctx for ctx in children if ctx.topic() != topic] |
|
2311 # XXX N-square membership on children |
|
2312 children = [ctx for ctx in children if ctx not in filtered] |
|
2313 displayer = cmdutil.show_changeset(ui, repo, {'template': shorttemplate}) |
|
2314 if len(children) == 1: |
|
2315 c = children[0] |
|
2316 bm = bmactive(repo) |
|
2317 shouldmove = opts.get('move_bookmark') and bm is not None |
|
2318 if dryrunopt: |
|
2319 ui.write(('hg update %s;\n' % c.rev())) |
|
2320 if shouldmove: |
|
2321 ui.write(('hg bookmark %s -r %s;\n' % (bm, c.rev()))) |
|
2322 else: |
|
2323 ret = hg.update(repo, c.rev()) |
|
2324 if not ret: |
|
2325 lock = tr = None |
|
2326 try: |
|
2327 lock = repo.lock() |
|
2328 tr = repo.transaction('next') |
|
2329 if shouldmove: |
|
2330 repo._bookmarks[bm] = c.node() |
|
2331 repo._bookmarks.recordchange(tr) |
|
2332 else: |
|
2333 bmdeactivate(repo) |
|
2334 tr.close() |
|
2335 finally: |
|
2336 lockmod.release(tr, lock) |
|
2337 displayer.show(c) |
|
2338 result = 0 |
|
2339 elif children: |
|
2340 ui.warn(_("ambigious next changeset:\n")) |
|
2341 for c in children: |
|
2342 displayer.show(c) |
|
2343 ui.warn(_('explicitly update to one of them\n')) |
|
2344 result = 1 |
|
2345 else: |
|
2346 aspchildren = _aspiringchildren(repo, [repo['.'].rev()]) |
|
2347 if topic: |
|
2348 filtered.extend(repo[c] for c in children |
|
2349 if repo[c].topic() != topic) |
|
2350 # XXX N-square membership on children |
|
2351 aspchildren = [ctx for ctx in aspchildren if ctx not in filtered] |
|
2352 if not opts['evolve'] or not aspchildren: |
|
2353 if filtered: |
|
2354 ui.warn(_('no children on topic "%s"\n') % topic) |
|
2355 ui.warn(_('do you want --no-topic\n')) |
|
2356 else: |
|
2357 ui.warn(_('no children\n')) |
|
2358 if aspchildren: |
|
2359 msg = _('(%i unstable changesets to be evolved here, ' |
|
2360 'do you want --evolve?)\n') |
|
2361 ui.warn(msg % len(aspchildren)) |
|
2362 result = 1 |
|
2363 elif 1 < len(aspchildren): |
|
2364 ui.warn(_("ambigious next (unstable) changeset:\n")) |
|
2365 for c in aspchildren: |
|
2366 displayer.show(repo[c]) |
|
2367 ui.warn(_("(run 'hg evolve --rev REV' on one of them)\n")) |
|
2368 return 1 |
|
2369 else: |
|
2370 cmdutil.bailifchanged(repo) |
|
2371 result = _solveone(ui, repo, repo[aspchildren[0]], dryrunopt, |
|
2372 False, lambda:None, category='unstable') |
|
2373 if not result: |
|
2374 ui.status(_('working directory now at %s\n') % repo['.']) |
|
2375 return result |
|
2376 return 1 |
|
2377 return result |
|
2378 finally: |
|
2379 lockmod.release(wlock) |
|
2380 |
|
2381 def _reachablefrombookmark(repo, revs, bookmarks): |
|
2382 """filter revisions and bookmarks reachable from the given bookmark |
|
2383 yoinked from mq.py |
|
2384 """ |
|
2385 repomarks = repo._bookmarks |
|
2386 if not bookmarks.issubset(repomarks): |
|
2387 raise error.Abort(_("bookmark '%s' not found") % |
|
2388 ','.join(sorted(bookmarks - set(repomarks.keys())))) |
|
2389 |
|
2390 # If the requested bookmark is not the only one pointing to a |
|
2391 # a revision we have to only delete the bookmark and not strip |
|
2392 # anything. revsets cannot detect that case. |
|
2393 nodetobookmarks = {} |
|
2394 for mark, node in repomarks.iteritems(): |
|
2395 nodetobookmarks.setdefault(node, []).append(mark) |
|
2396 for marks in nodetobookmarks.values(): |
|
2397 if bookmarks.issuperset(marks): |
|
2398 if util.safehasattr(repair, 'stripbmrevset'): |
|
2399 rsrevs = repair.stripbmrevset(repo, marks[0]) |
|
2400 else: |
|
2401 rsrevs = repo.revs("ancestors(bookmark(%s)) - " |
|
2402 "ancestors(head() and not bookmark(%s)) - " |
|
2403 "ancestors(bookmark() and not bookmark(%s)) - " |
|
2404 "obsolete()", |
|
2405 marks[0], marks[0], marks[0]) |
|
2406 revs = set(revs) |
|
2407 revs.update(set(rsrevs)) |
|
2408 revs = sorted(revs) |
|
2409 return repomarks, revs |
|
2410 |
|
2411 def _deletebookmark(repo, repomarks, bookmarks): |
|
2412 wlock = lock = tr = None |
|
2413 try: |
|
2414 wlock = repo.wlock() |
|
2415 lock = repo.lock() |
|
2416 tr = repo.transaction('prune') |
|
2417 for bookmark in bookmarks: |
|
2418 del repomarks[bookmark] |
|
2419 repomarks.recordchange(tr) |
|
2420 tr.close() |
|
2421 for bookmark in sorted(bookmarks): |
|
2422 repo.ui.write(_("bookmark '%s' deleted\n") % bookmark) |
|
2423 finally: |
|
2424 lockmod.release(tr, lock, wlock) |
|
2425 |
|
2426 |
|
2427 |
|
2428 def _getmetadata(**opts): |
|
2429 metadata = {} |
|
2430 date = opts.get('date') |
|
2431 user = opts.get('user') |
|
2432 if date: |
|
2433 metadata['date'] = '%i %i' % util.parsedate(date) |
|
2434 if user: |
|
2435 metadata['user'] = user |
|
2436 return metadata |
|
2437 |
|
2438 |
|
2439 @command('^prune|obsolete', |
|
2440 [('n', 'new', [], _("successor changeset (DEPRECATED)")), |
|
2441 ('s', 'succ', [], _("successor changeset")), |
|
2442 ('r', 'rev', [], _("revisions to prune")), |
|
2443 ('k', 'keep', None, _("does not modify working copy during prune")), |
|
2444 ('', 'biject', False, _("do a 1-1 map between rev and successor ranges")), |
|
2445 ('', 'fold', False, |
|
2446 _("record a fold (multiple precursors, one successors)")), |
|
2447 ('', 'split', False, |
|
2448 _("record a split (on precursor, multiple successors)")), |
|
2449 ('B', 'bookmark', [], _("remove revs only reachable from given" |
|
2450 " bookmark"))] + metadataopts, |
|
2451 _('[OPTION] [-r] REV...')) |
|
2452 # -U --noupdate option to prevent wc update and or bookmarks update ? |
|
2453 def cmdprune(ui, repo, *revs, **opts): |
|
2454 """hide changesets by marking them obsolete |
|
2455 |
|
2456 Pruned changesets are obsolete with no successors. If they also have no |
|
2457 descendants, they are hidden (invisible to all commands). |
|
2458 |
|
2459 Non-obsolete descendants of pruned changesets become "unstable". Use :hg:`evolve` |
|
2460 to handle this situation. |
|
2461 |
|
2462 When you prune the parent of your working copy, Mercurial updates the working |
|
2463 copy to a non-obsolete parent. |
|
2464 |
|
2465 You can use ``--succ`` to tell Mercurial that a newer version (successor) of the |
|
2466 pruned changeset exists. Mercurial records successor revisions in obsolescence |
|
2467 markers. |
|
2468 |
|
2469 You can use the ``--biject`` option to specify a 1-1 mapping (bijection) between |
|
2470 revisions to pruned (precursor) and successor changesets. This option may be |
|
2471 removed in a future release (with the functionality provided automatically). |
|
2472 |
|
2473 If you specify multiple revisions in ``--succ``, you are recording a "split" and |
|
2474 must acknowledge it by passing ``--split``. Similarly, when you prune multiple |
|
2475 changesets with a single successor, you must pass the ``--fold`` option. |
|
2476 """ |
|
2477 revs = scmutil.revrange(repo, list(revs) + opts.get('rev')) |
|
2478 succs = opts['new'] + opts['succ'] |
|
2479 bookmarks = set(opts.get('bookmark')) |
|
2480 metadata = _getmetadata(**opts) |
|
2481 biject = opts.get('biject') |
|
2482 fold = opts.get('fold') |
|
2483 split = opts.get('split') |
|
2484 |
|
2485 options = [o for o in ('biject', 'fold', 'split') if opts.get(o)] |
|
2486 if 1 < len(options): |
|
2487 raise error.Abort(_("can only specify one of %s") % ', '.join(options)) |
|
2488 |
|
2489 if bookmarks: |
|
2490 repomarks, revs = _reachablefrombookmark(repo, revs, bookmarks) |
|
2491 if not revs: |
|
2492 # no revisions to prune - delete bookmark immediately |
|
2493 _deletebookmark(repo, repomarks, bookmarks) |
|
2494 |
|
2495 if not revs: |
|
2496 raise error.Abort(_('nothing to prune')) |
|
2497 |
|
2498 wlock = lock = tr = None |
|
2499 try: |
|
2500 wlock = repo.wlock() |
|
2501 lock = repo.lock() |
|
2502 tr = repo.transaction('prune') |
|
2503 # defines pruned changesets |
|
2504 precs = [] |
|
2505 revs.sort() |
|
2506 for p in revs: |
|
2507 cp = repo[p] |
|
2508 if not cp.mutable(): |
|
2509 # note: createmarkers() would have raised something anyway |
|
2510 raise error.Abort('cannot prune immutable changeset: %s' % cp, |
|
2511 hint="see 'hg help phases' for details") |
|
2512 precs.append(cp) |
|
2513 if not precs: |
|
2514 raise error.Abort('nothing to prune') |
|
2515 |
|
2516 if _disallowednewunstable(repo, revs): |
|
2517 raise error.Abort(_("cannot prune in the middle of a stack"), |
|
2518 hint = _("new unstable changesets are not allowed")) |
|
2519 |
|
2520 # defines successors changesets |
|
2521 sucs = scmutil.revrange(repo, succs) |
|
2522 sucs.sort() |
|
2523 sucs = tuple(repo[n] for n in sucs) |
|
2524 if not biject and len(sucs) > 1 and len(precs) > 1: |
|
2525 msg = "Can't use multiple successors for multiple precursors" |
|
2526 hint = _("use --biject to mark a series as a replacement" |
|
2527 " for another") |
|
2528 raise error.Abort(msg, hint=hint) |
|
2529 elif biject and len(sucs) != len(precs): |
|
2530 msg = "Can't use %d successors for %d precursors" \ |
|
2531 % (len(sucs), len(precs)) |
|
2532 raise error.Abort(msg) |
|
2533 elif (len(precs) == 1 and len(sucs) > 1) and not split: |
|
2534 msg = "please add --split if you want to do a split" |
|
2535 raise error.Abort(msg) |
|
2536 elif len(sucs) == 1 and len(precs) > 1 and not fold: |
|
2537 msg = "please add --fold if you want to do a fold" |
|
2538 raise error.Abort(msg) |
|
2539 elif biject: |
|
2540 relations = [(p, (s,)) for p, s in zip(precs, sucs)] |
|
2541 else: |
|
2542 relations = [(p, sucs) for p in precs] |
|
2543 |
|
2544 wdp = repo['.'] |
|
2545 |
|
2546 if len(sucs) == 1 and len(precs) == 1 and wdp in precs: |
|
2547 # '.' killed, so update to the successor |
|
2548 newnode = sucs[0] |
|
2549 else: |
|
2550 # update to an unkilled parent |
|
2551 newnode = wdp |
|
2552 |
|
2553 while newnode in precs or newnode.obsolete(): |
|
2554 newnode = newnode.parents()[0] |
|
2555 |
|
2556 |
|
2557 if newnode.node() != wdp.node(): |
|
2558 if opts.get('keep', False): |
|
2559 # This is largely the same as the implementation in |
|
2560 # strip.stripcmd(). We might want to refactor this somewhere |
|
2561 # common at some point. |
|
2562 |
|
2563 # only reset the dirstate for files that would actually change |
|
2564 # between the working context and uctx |
|
2565 descendantrevs = repo.revs("%d::." % newnode.rev()) |
|
2566 changedfiles = [] |
|
2567 for rev in descendantrevs: |
|
2568 # blindly reset the files, regardless of what actually |
|
2569 # changed |
|
2570 changedfiles.extend(repo[rev].files()) |
|
2571 |
|
2572 # reset files that only changed in the dirstate too |
|
2573 dirstate = repo.dirstate |
|
2574 dirchanges = [f for f in dirstate if dirstate[f] != 'n'] |
|
2575 changedfiles.extend(dirchanges) |
|
2576 repo.dirstate.rebuild(newnode.node(), newnode.manifest(), |
|
2577 changedfiles) |
|
2578 writedirstate(dirstate, tr) |
|
2579 else: |
|
2580 bookactive = bmactive(repo) |
|
2581 # Active bookmark that we don't want to delete (with -B option) |
|
2582 # we deactivate and move it before the update and reactivate it |
|
2583 # after |
|
2584 movebookmark = bookactive and not bookmarks |
|
2585 if movebookmark: |
|
2586 bmdeactivate(repo) |
|
2587 repo._bookmarks[bookactive] = newnode.node() |
|
2588 repo._bookmarks.recordchange(tr) |
|
2589 commands.update(ui, repo, newnode.rev()) |
|
2590 ui.status(_('working directory now at %s\n') % newnode) |
|
2591 if movebookmark: |
|
2592 bmactivate(repo, bookactive) |
|
2593 |
|
2594 # update bookmarks |
|
2595 if bookmarks: |
|
2596 _deletebookmark(repo, repomarks, bookmarks) |
|
2597 |
|
2598 # create markers |
|
2599 obsolete.createmarkers(repo, relations, metadata=metadata) |
|
2600 |
|
2601 # informs that changeset have been pruned |
|
2602 ui.status(_('%i changesets pruned\n') % len(precs)) |
|
2603 |
|
2604 for ctx in repo.unfiltered().set('bookmark() and %ld', precs): |
|
2605 # used to be: |
|
2606 # |
|
2607 # ldest = list(repo.set('max((::%d) - obsolete())', ctx)) |
|
2608 # if ldest: |
|
2609 # c = ldest[0] |
|
2610 # |
|
2611 # but then revset took a lazy arrow in the knee and became much |
|
2612 # slower. The new forms makes as much sense and a much faster. |
|
2613 for dest in ctx.ancestors(): |
|
2614 if not dest.obsolete(): |
|
2615 updatebookmarks = _bookmarksupdater(repo, ctx.node(), tr) |
|
2616 updatebookmarks(dest.node()) |
|
2617 break |
|
2618 |
|
2619 tr.close() |
|
2620 finally: |
|
2621 lockmod.release(tr, lock, wlock) |
|
2622 |
|
2623 @command('amend|refresh', |
|
2624 [('A', 'addremove', None, |
|
2625 _('mark new/missing files as added/removed before committing')), |
|
2626 ('e', 'edit', False, _('invoke editor on commit messages')), |
|
2627 ('', 'close-branch', None, |
|
2628 _('mark a branch as closed, hiding it from the branch list')), |
|
2629 ('s', 'secret', None, _('use the secret phase for committing')), |
|
2630 ] + walkopts + commitopts + commitopts2 + commitopts3 + interactiveopt, |
|
2631 _('[OPTION]... [FILE]...')) |
|
2632 def amend(ui, repo, *pats, **opts): |
|
2633 """combine a changeset with updates and replace it with a new one |
|
2634 |
|
2635 Commits a new changeset incorporating both the changes to the given files |
|
2636 and all the changes from the current parent changeset into the repository. |
|
2637 |
|
2638 See :hg:`commit` for details about committing changes. |
|
2639 |
|
2640 If you don't specify -m, the parent's message will be reused. |
|
2641 |
|
2642 Behind the scenes, Mercurial first commits the update as a regular child |
|
2643 of the current parent. Then it creates a new commit on the parent's parents |
|
2644 with the updated contents. Then it changes the working copy parent to this |
|
2645 new combined changeset. Finally, the old changeset and its update are hidden |
|
2646 from :hg:`log` (unless you use --hidden with log). |
|
2647 |
|
2648 Returns 0 on success, 1 if nothing changed. |
|
2649 """ |
|
2650 opts = opts.copy() |
|
2651 edit = opts.pop('edit', False) |
|
2652 log = opts.get('logfile') |
|
2653 opts['amend'] = True |
|
2654 if not (edit or opts['message'] or log): |
|
2655 opts['message'] = repo['.'].description() |
|
2656 _resolveoptions(ui, opts) |
|
2657 _alias, commitcmd = cmdutil.findcmd('commit', commands.table) |
|
2658 return commitcmd[0](ui, repo, *pats, **opts) |
|
2659 |
|
2660 |
|
2661 def _touchedbetween(repo, source, dest, match=None): |
|
2662 touched = set() |
|
2663 for files in repo.status(source, dest, match=match)[:3]: |
|
2664 touched.update(files) |
|
2665 return touched |
|
2666 |
|
2667 def _commitfiltered(repo, ctx, match, target=None): |
|
2668 """Recommit ctx with changed files not in match. Return the new |
|
2669 node identifier, or None if nothing changed. |
|
2670 """ |
|
2671 base = ctx.p1() |
|
2672 if target is None: |
|
2673 target = base |
|
2674 # ctx |
|
2675 initialfiles = _touchedbetween(repo, base, ctx) |
|
2676 if base == target: |
|
2677 affected = set(f for f in initialfiles if match(f)) |
|
2678 newcontent = set() |
|
2679 else: |
|
2680 affected = _touchedbetween(repo, target, ctx, match=match) |
|
2681 newcontent = _touchedbetween(repo, target, base, match=match) |
|
2682 # The commit touchs all existing files |
|
2683 # + all file that needs a new content |
|
2684 # - the file affected bny uncommit with the same content than base. |
|
2685 files = (initialfiles - affected) | newcontent |
|
2686 if not newcontent and files == initialfiles: |
|
2687 return None |
|
2688 |
|
2689 # Filter copies |
|
2690 copied = copies.pathcopies(target, ctx) |
|
2691 copied = dict((dst, src) for dst, src in copied.iteritems() |
|
2692 if dst in files) |
|
2693 def filectxfn(repo, memctx, path, contentctx=ctx, redirect=newcontent): |
|
2694 if path in redirect: |
|
2695 return filectxfn(repo, memctx, path, contentctx=target, redirect=()) |
|
2696 if path not in contentctx: |
|
2697 return None |
|
2698 fctx = contentctx[path] |
|
2699 flags = fctx.flags() |
|
2700 mctx = memfilectx(repo, fctx.path(), fctx.data(), |
|
2701 islink='l' in flags, |
|
2702 isexec='x' in flags, |
|
2703 copied=copied.get(path)) |
|
2704 return mctx |
|
2705 |
|
2706 new = context.memctx(repo, |
|
2707 parents=[base.node(), node.nullid], |
|
2708 text=ctx.description(), |
|
2709 files=files, |
|
2710 filectxfn=filectxfn, |
|
2711 user=ctx.user(), |
|
2712 date=ctx.date(), |
|
2713 extra=ctx.extra()) |
|
2714 # commitctx always create a new revision, no need to check |
|
2715 newid = repo.commitctx(new) |
|
2716 return newid |
|
2717 |
|
2718 def _uncommitdirstate(repo, oldctx, match): |
|
2719 """Fix the dirstate after switching the working directory from |
|
2720 oldctx to a copy of oldctx not containing changed files matched by |
|
2721 match. |
|
2722 """ |
|
2723 ctx = repo['.'] |
|
2724 ds = repo.dirstate |
|
2725 copies = dict(ds.copies()) |
|
2726 m, a, r = repo.status(oldctx.p1(), oldctx, match=match)[:3] |
|
2727 for f in m: |
|
2728 if ds[f] == 'r': |
|
2729 # modified + removed -> removed |
|
2730 continue |
|
2731 ds.normallookup(f) |
|
2732 |
|
2733 for f in a: |
|
2734 if ds[f] == 'r': |
|
2735 # added + removed -> unknown |
|
2736 ds.drop(f) |
|
2737 elif ds[f] != 'a': |
|
2738 ds.add(f) |
|
2739 |
|
2740 for f in r: |
|
2741 if ds[f] == 'a': |
|
2742 # removed + added -> normal |
|
2743 ds.normallookup(f) |
|
2744 elif ds[f] != 'r': |
|
2745 ds.remove(f) |
|
2746 |
|
2747 # Merge old parent and old working dir copies |
|
2748 oldcopies = {} |
|
2749 for f in (m + a): |
|
2750 src = oldctx[f].renamed() |
|
2751 if src: |
|
2752 oldcopies[f] = src[0] |
|
2753 oldcopies.update(copies) |
|
2754 copies = dict((dst, oldcopies.get(src, src)) |
|
2755 for dst, src in oldcopies.iteritems()) |
|
2756 # Adjust the dirstate copies |
|
2757 for dst, src in copies.iteritems(): |
|
2758 if (src not in ctx or dst in ctx or ds[dst] != 'a'): |
|
2759 src = None |
|
2760 ds.copy(src, dst) |
|
2761 |
|
2762 @command('^uncommit', |
|
2763 [('a', 'all', None, _('uncommit all changes when no arguments given')), |
|
2764 ('r', 'rev', '', _('revert commit content to REV instead')), |
|
2765 ] + commands.walkopts, |
|
2766 _('[OPTION]... [NAME]')) |
|
2767 def uncommit(ui, repo, *pats, **opts): |
|
2768 """move changes from parent revision to working directory |
|
2769 |
|
2770 Changes to selected files in the checked out revision appear again as |
|
2771 uncommitted changed in the working directory. A new revision |
|
2772 without the selected changes is created, becomes the checked out |
|
2773 revision, and obsoletes the previous one. |
|
2774 |
|
2775 The --include option specifies patterns to uncommit. |
|
2776 The --exclude option specifies patterns to keep in the commit. |
|
2777 |
|
2778 The --rev argument let you change the commit file to a content of another |
|
2779 revision. It still does not change the content of your file in the working |
|
2780 directory. |
|
2781 |
|
2782 Return 0 if changed files are uncommitted. |
|
2783 """ |
|
2784 |
|
2785 wlock = lock = tr = None |
|
2786 try: |
|
2787 wlock = repo.wlock() |
|
2788 lock = repo.lock() |
|
2789 wctx = repo[None] |
|
2790 if len(wctx.parents()) <= 0: |
|
2791 raise error.Abort(_("cannot uncommit null changeset")) |
|
2792 if len(wctx.parents()) > 1: |
|
2793 raise error.Abort(_("cannot uncommit while merging")) |
|
2794 old = repo['.'] |
|
2795 if old.phase() == phases.public: |
|
2796 raise error.Abort(_("cannot rewrite immutable changeset")) |
|
2797 if len(old.parents()) > 1: |
|
2798 raise error.Abort(_("cannot uncommit merge changeset")) |
|
2799 oldphase = old.phase() |
|
2800 |
|
2801 |
|
2802 rev = None |
|
2803 if opts.get('rev'): |
|
2804 rev = scmutil.revsingle(repo, opts.get('rev')) |
|
2805 ctx = repo[None] |
|
2806 if ctx.p1() == rev or ctx.p2() == rev: |
|
2807 raise error.Abort(_("cannot uncommit to parent changeset")) |
|
2808 |
|
2809 onahead = old.rev() in repo.changelog.headrevs() |
|
2810 disallowunstable = not obsolete.isenabled(repo, |
|
2811 obsolete.allowunstableopt) |
|
2812 if disallowunstable and not onahead: |
|
2813 raise error.Abort(_("cannot uncommit in the middle of a stack")) |
|
2814 |
|
2815 # Recommit the filtered changeset |
|
2816 tr = repo.transaction('uncommit') |
|
2817 updatebookmarks = _bookmarksupdater(repo, old.node(), tr) |
|
2818 newid = None |
|
2819 includeorexclude = opts.get('include') or opts.get('exclude') |
|
2820 if (pats or includeorexclude or opts.get('all')): |
|
2821 match = scmutil.match(old, pats, opts) |
|
2822 newid = _commitfiltered(repo, old, match, target=rev) |
|
2823 if newid is None: |
|
2824 raise error.Abort(_('nothing to uncommit'), |
|
2825 hint=_("use --all to uncommit all files")) |
|
2826 # Move local changes on filtered changeset |
|
2827 obsolete.createmarkers(repo, [(old, (repo[newid],))]) |
|
2828 phases.retractboundary(repo, tr, oldphase, [newid]) |
|
2829 repo.dirstate.beginparentchange() |
|
2830 repo.dirstate.setparents(newid, node.nullid) |
|
2831 _uncommitdirstate(repo, old, match) |
|
2832 repo.dirstate.endparentchange() |
|
2833 updatebookmarks(newid) |
|
2834 if not repo[newid].files(): |
|
2835 ui.warn(_("new changeset is empty\n")) |
|
2836 ui.status(_("(use 'hg prune .' to remove it)\n")) |
|
2837 tr.close() |
|
2838 finally: |
|
2839 lockmod.release(tr, lock, wlock) |
|
2840 |
|
2841 @eh.wrapcommand('commit') |
|
2842 def commitwrapper(orig, ui, repo, *arg, **kwargs): |
|
2843 tr = None |
|
2844 if kwargs.get('amend', False): |
|
2845 wlock = lock = None |
|
2846 else: |
|
2847 wlock = repo.wlock() |
|
2848 lock = repo.lock() |
|
2849 try: |
|
2850 obsoleted = kwargs.get('obsolete', []) |
|
2851 if obsoleted: |
|
2852 obsoleted = repo.set('%lr', obsoleted) |
|
2853 result = orig(ui, repo, *arg, **kwargs) |
|
2854 if not result: # commit succeeded |
|
2855 new = repo['-1'] |
|
2856 oldbookmarks = [] |
|
2857 markers = [] |
|
2858 for old in obsoleted: |
|
2859 oldbookmarks.extend(repo.nodebookmarks(old.node())) |
|
2860 markers.append((old, (new,))) |
|
2861 if markers: |
|
2862 obsolete.createmarkers(repo, markers) |
|
2863 for book in oldbookmarks: |
|
2864 repo._bookmarks[book] = new.node() |
|
2865 if oldbookmarks: |
|
2866 if not wlock: |
|
2867 wlock = repo.wlock() |
|
2868 if not lock: |
|
2869 lock = repo.lock() |
|
2870 tr = repo.transaction('commit') |
|
2871 repo._bookmarks.recordchange(tr) |
|
2872 tr.close() |
|
2873 return result |
|
2874 finally: |
|
2875 lockmod.release(tr, lock, wlock) |
|
2876 |
|
2877 @command('^split', |
|
2878 [('r', 'rev', [], _("revision to split")), |
|
2879 ] + commitopts + commitopts2, |
|
2880 _('hg split [OPTION]... [-r] REV')) |
|
2881 def cmdsplit(ui, repo, *revs, **opts): |
|
2882 """split a changeset into smaller changesets |
|
2883 |
|
2884 By default, split the current revision by prompting for all its hunks to be |
|
2885 redistributed into new changesets. |
|
2886 |
|
2887 Use --rev to split a given changeset instead. |
|
2888 """ |
|
2889 tr = wlock = lock = None |
|
2890 newcommits = [] |
|
2891 |
|
2892 revarg = (list(revs) + opts.get('rev')) or ['.'] |
|
2893 if len(revarg) != 1: |
|
2894 msg = _("more than one revset is given") |
|
2895 hnt = _("use either `hg split <rs>` or `hg split --rev <rs>`, not both") |
|
2896 raise error.Abort(msg, hint=hnt) |
|
2897 |
|
2898 rev = scmutil.revsingle(repo, revarg[0]) |
|
2899 try: |
|
2900 wlock = repo.wlock() |
|
2901 lock = repo.lock() |
|
2902 cmdutil.bailifchanged(repo) |
|
2903 tr = repo.transaction('split') |
|
2904 ctx = repo[rev] |
|
2905 r = ctx.rev() |
|
2906 disallowunstable = not obsolete.isenabled(repo, |
|
2907 obsolete.allowunstableopt) |
|
2908 if disallowunstable: |
|
2909 # XXX We should check head revs |
|
2910 if repo.revs("(%d::) - %d", rev, rev): |
|
2911 raise error.Abort(_("cannot split commit: %s not a head") % ctx) |
|
2912 |
|
2913 if len(ctx.parents()) > 1: |
|
2914 raise error.Abort(_("cannot split merge commits")) |
|
2915 prev = ctx.p1() |
|
2916 bmupdate = _bookmarksupdater(repo, ctx.node(), tr) |
|
2917 bookactive = bmactive(repo) |
|
2918 if bookactive is not None: |
|
2919 repo.ui.status(_("(leaving bookmark %s)\n") % bmactive(repo)) |
|
2920 bmdeactivate(repo) |
|
2921 hg.update(repo, prev) |
|
2922 |
|
2923 commands.revert(ui, repo, rev=r, all=True) |
|
2924 def haschanges(): |
|
2925 modified, added, removed, deleted = repo.status()[:4] |
|
2926 return modified or added or removed or deleted |
|
2927 msg = ("HG: This is the original pre-split commit message. " |
|
2928 "Edit it as appropriate.\n\n") |
|
2929 msg += ctx.description() |
|
2930 opts['message'] = msg |
|
2931 opts['edit'] = True |
|
2932 while haschanges(): |
|
2933 pats = () |
|
2934 cmdutil.dorecord(ui, repo, commands.commit, 'commit', False, |
|
2935 cmdutil.recordfilter, *pats, **opts) |
|
2936 # TODO: Does no seem like the best way to do this |
|
2937 # We should make dorecord return the newly created commit |
|
2938 newcommits.append(repo['.']) |
|
2939 if haschanges(): |
|
2940 if ui.prompt('Done splitting? [yN]', default='n') == 'y': |
|
2941 commands.commit(ui, repo, **opts) |
|
2942 newcommits.append(repo['.']) |
|
2943 break |
|
2944 else: |
|
2945 ui.status(_("no more change to split\n")) |
|
2946 |
|
2947 if newcommits: |
|
2948 tip = repo[newcommits[-1]] |
|
2949 bmupdate(tip.node()) |
|
2950 if bookactive is not None: |
|
2951 bmactivate(repo, bookactive) |
|
2952 obsolete.createmarkers(repo, [(repo[r], newcommits)]) |
|
2953 tr.close() |
|
2954 finally: |
|
2955 lockmod.release(tr, lock, wlock) |
|
2956 |
|
2957 |
|
2958 @eh.wrapcommand('strip', extension='strip', opts=[ |
|
2959 ('', 'bundle', None, _("delete the commit entirely and move it to a " |
|
2960 "backup bundle")), |
|
2961 ]) |
|
2962 def stripwrapper(orig, ui, repo, *revs, **kwargs): |
|
2963 if (not ui.configbool('experimental', 'prunestrip') or |
|
2964 kwargs.get('bundle', False)): |
|
2965 return orig(ui, repo, *revs, **kwargs) |
|
2966 |
|
2967 if kwargs.get('force'): |
|
2968 ui.warn(_("warning: --force has no effect during strip with evolve " |
|
2969 "enabled\n")) |
|
2970 if kwargs.get('no_backup', False): |
|
2971 ui.warn(_("warning: --no-backup has no effect during strips with " |
|
2972 "evolve enabled\n")) |
|
2973 |
|
2974 revs = list(revs) + kwargs.pop('rev', []) |
|
2975 revs = set(scmutil.revrange(repo, revs)) |
|
2976 revs = repo.revs("(%ld)::", revs) |
|
2977 kwargs['rev'] = [] |
|
2978 kwargs['new'] = [] |
|
2979 kwargs['succ'] = [] |
|
2980 kwargs['biject'] = False |
|
2981 return cmdprune(ui, repo, *revs, **kwargs) |
|
2982 |
|
2983 @command('^touch', |
|
2984 [('r', 'rev', [], 'revision to update'), |
|
2985 ('D', 'duplicate', False, |
|
2986 'do not mark the new revision as successor of the old one'), |
|
2987 ('A', 'allowdivergence', False, |
|
2988 'mark the new revision as successor of the old one potentially creating ' |
|
2989 'divergence')], |
|
2990 # allow to choose the seed ? |
|
2991 _('[-r] revs')) |
|
2992 def touch(ui, repo, *revs, **opts): |
|
2993 """create successors that are identical to their predecessors except |
|
2994 for the changeset ID |
|
2995 |
|
2996 This is used to "resurrect" changesets |
|
2997 """ |
|
2998 duplicate = opts['duplicate'] |
|
2999 allowdivergence = opts['allowdivergence'] |
|
3000 revs = list(revs) |
|
3001 revs.extend(opts['rev']) |
|
3002 if not revs: |
|
3003 revs = ['.'] |
|
3004 revs = scmutil.revrange(repo, revs) |
|
3005 if not revs: |
|
3006 ui.write_err('no revision to touch\n') |
|
3007 return 1 |
|
3008 if not duplicate and repo.revs('public() and %ld', revs): |
|
3009 raise error.Abort("can't touch public revision") |
|
3010 displayer = cmdutil.show_changeset(ui, repo, {'template': shorttemplate}) |
|
3011 wlock = lock = tr = None |
|
3012 try: |
|
3013 wlock = repo.wlock() |
|
3014 lock = repo.lock() |
|
3015 tr = repo.transaction('touch') |
|
3016 revs.sort() # ensure parent are run first |
|
3017 newmapping = {} |
|
3018 for r in revs: |
|
3019 ctx = repo[r] |
|
3020 extra = ctx.extra().copy() |
|
3021 extra['__touch-noise__'] = random.randint(0, 0xffffffff) |
|
3022 # search for touched parent |
|
3023 p1 = ctx.p1().node() |
|
3024 p2 = ctx.p2().node() |
|
3025 p1 = newmapping.get(p1, p1) |
|
3026 p2 = newmapping.get(p2, p2) |
|
3027 |
|
3028 if not (duplicate or allowdivergence): |
|
3029 # The user hasn't yet decided what to do with the revived |
|
3030 # cset, let's ask |
|
3031 sset = obsolete.successorssets(repo, ctx.node()) |
|
3032 nodivergencerisk = len(sset) == 0 or ( |
|
3033 len(sset) == 1 and |
|
3034 len(sset[0]) == 1 and |
|
3035 repo[sset[0][0]].rev() == ctx.rev() |
|
3036 ) |
|
3037 if nodivergencerisk: |
|
3038 duplicate = False |
|
3039 else: |
|
3040 displayer.show(ctx) |
|
3041 index = ui.promptchoice( |
|
3042 _("reviving this changeset will create divergence" |
|
3043 " unless you make a duplicate.\n(a)llow divergence or" |
|
3044 " (d)uplicate the changeset? $$ &Allowdivergence $$ " |
|
3045 "&Duplicate"), 0) |
|
3046 choice = ['allowdivergence', 'duplicate'][index] |
|
3047 if choice == 'allowdivergence': |
|
3048 duplicate = False |
|
3049 else: |
|
3050 duplicate = True |
|
3051 |
|
3052 new, unusedvariable = rewrite(repo, ctx, [], ctx, |
|
3053 [p1, p2], |
|
3054 commitopts={'extra': extra}) |
|
3055 # store touched version to help potential children |
|
3056 newmapping[ctx.node()] = new |
|
3057 |
|
3058 if not duplicate: |
|
3059 obsolete.createmarkers(repo, [(ctx, (repo[new],))]) |
|
3060 phases.retractboundary(repo, tr, ctx.phase(), [new]) |
|
3061 if ctx in repo[None].parents(): |
|
3062 repo.dirstate.beginparentchange() |
|
3063 repo.dirstate.setparents(new, node.nullid) |
|
3064 repo.dirstate.endparentchange() |
|
3065 tr.close() |
|
3066 finally: |
|
3067 lockmod.release(tr, lock, wlock) |
|
3068 |
|
3069 @command('^fold|squash', |
|
3070 [('r', 'rev', [], _("revision to fold")), |
|
3071 ('', 'exact', None, _("only fold specified revisions")), |
|
3072 ('', 'from', None, _("fold revisions linearly to working copy parent")) |
|
3073 ] + commitopts + commitopts2, |
|
3074 _('hg fold [OPTION]... [-r] REV')) |
|
3075 def fold(ui, repo, *revs, **opts): |
|
3076 """fold multiple revisions into a single one |
|
3077 |
|
3078 With --from, folds all the revisions linearly between the given revisions |
|
3079 and the parent of the working directory. |
|
3080 |
|
3081 With --exact, folds only the specified revisions while ignoring the |
|
3082 parent of the working directory. In this case, the given revisions must |
|
3083 form a linear unbroken chain. |
|
3084 |
|
3085 .. container:: verbose |
|
3086 |
|
3087 Some examples: |
|
3088 |
|
3089 - Fold the current revision with its parent:: |
|
3090 |
|
3091 hg fold --from .^ |
|
3092 |
|
3093 - Fold all draft revisions with working directory parent:: |
|
3094 |
|
3095 hg fold --from 'draft()' |
|
3096 |
|
3097 See :hg:`help phases` for more about draft revisions and |
|
3098 :hg:`help revsets` for more about the `draft()` keyword |
|
3099 |
|
3100 - Fold revisions between 3 and 6 with the working directory parent:: |
|
3101 |
|
3102 hg fold --from 3::6 |
|
3103 |
|
3104 - Fold revisions 3 and 4: |
|
3105 |
|
3106 hg fold "3 + 4" --exact |
|
3107 |
|
3108 - Only fold revisions linearly between foo and @:: |
|
3109 |
|
3110 hg fold foo::@ --exact |
|
3111 """ |
|
3112 revs = list(revs) |
|
3113 revs.extend(opts['rev']) |
|
3114 if not revs: |
|
3115 raise error.Abort(_('no revisions specified')) |
|
3116 |
|
3117 revs = scmutil.revrange(repo, revs) |
|
3118 |
|
3119 if opts['from'] and opts['exact']: |
|
3120 raise error.Abort(_('cannot use both --from and --exact')) |
|
3121 elif opts['from']: |
|
3122 # Try to extend given revision starting from the working directory |
|
3123 extrevs = repo.revs('(%ld::.) or (.::%ld)', revs, revs) |
|
3124 discardedrevs = [r for r in revs if r not in extrevs] |
|
3125 if discardedrevs: |
|
3126 raise error.Abort(_("cannot fold non-linear revisions"), |
|
3127 hint=_("given revisions are unrelated to parent " |
|
3128 "of working directory")) |
|
3129 revs = extrevs |
|
3130 elif opts['exact']: |
|
3131 # Nothing to do; "revs" is already set correctly |
|
3132 pass |
|
3133 else: |
|
3134 raise error.Abort(_('must specify either --from or --exact')) |
|
3135 |
|
3136 if not revs: |
|
3137 raise error.Abort(_('specified revisions evaluate to an empty set'), |
|
3138 hint=_('use different revision arguments')) |
|
3139 elif len(revs) == 1: |
|
3140 ui.write_err(_('single revision specified, nothing to fold\n')) |
|
3141 return 1 |
|
3142 |
|
3143 wlock = lock = None |
|
3144 try: |
|
3145 wlock = repo.wlock() |
|
3146 lock = repo.lock() |
|
3147 |
|
3148 root, head = _foldcheck(repo, revs) |
|
3149 |
|
3150 tr = repo.transaction('fold') |
|
3151 try: |
|
3152 commitopts = opts.copy() |
|
3153 allctx = [repo[r] for r in revs] |
|
3154 targetphase = max(c.phase() for c in allctx) |
|
3155 |
|
3156 if commitopts.get('message') or commitopts.get('logfile'): |
|
3157 commitopts['edit'] = False |
|
3158 else: |
|
3159 msgs = ["HG: This is a fold of %d changesets." % len(allctx)] |
|
3160 msgs += ["HG: Commit message of changeset %s.\n\n%s\n" % |
|
3161 (c.rev(), c.description()) for c in allctx] |
|
3162 commitopts['message'] = "\n".join(msgs) |
|
3163 commitopts['edit'] = True |
|
3164 |
|
3165 newid, unusedvariable = rewrite(repo, root, allctx, head, |
|
3166 [root.p1().node(), |
|
3167 root.p2().node()], |
|
3168 commitopts=commitopts) |
|
3169 phases.retractboundary(repo, tr, targetphase, [newid]) |
|
3170 obsolete.createmarkers(repo, [(ctx, (repo[newid],)) |
|
3171 for ctx in allctx]) |
|
3172 tr.close() |
|
3173 finally: |
|
3174 tr.release() |
|
3175 ui.status('%i changesets folded\n' % len(revs)) |
|
3176 if repo['.'].rev() in revs: |
|
3177 hg.update(repo, newid) |
|
3178 finally: |
|
3179 lockmod.release(lock, wlock) |
|
3180 |
|
3181 @command('^metaedit', |
|
3182 [('r', 'rev', [], _("revision to edit")), |
|
3183 ('', 'fold', None, _("also fold specified revisions into one")), |
|
3184 ] + commitopts + commitopts2, |
|
3185 _('hg metaedit [OPTION]... [-r] [REV]')) |
|
3186 def metaedit(ui, repo, *revs, **opts): |
|
3187 """edit commit information |
|
3188 |
|
3189 Edits the commit information for the specified revisions. By default, edits |
|
3190 commit information for the working directory parent. |
|
3191 |
|
3192 With --fold, also folds multiple revisions into one if necessary. In this |
|
3193 case, the given revisions must form a linear unbroken chain. |
|
3194 |
|
3195 .. container:: verbose |
|
3196 |
|
3197 Some examples: |
|
3198 |
|
3199 - Edit the commit message for the working directory parent:: |
|
3200 |
|
3201 hg metaedit |
|
3202 |
|
3203 - Change the username for the working directory parent:: |
|
3204 |
|
3205 hg metaedit --user 'New User <new-email@example.com>' |
|
3206 |
|
3207 - Combine all draft revisions that are ancestors of foo but not of @ into |
|
3208 one:: |
|
3209 |
|
3210 hg metaedit --fold 'draft() and only(foo,@)' |
|
3211 |
|
3212 See :hg:`help phases` for more about draft revisions, and |
|
3213 :hg:`help revsets` for more about the `draft()` and `only()` keywords. |
|
3214 """ |
|
3215 revs = list(revs) |
|
3216 revs.extend(opts['rev']) |
|
3217 if not revs: |
|
3218 if opts['fold']: |
|
3219 raise error.Abort(_('revisions must be specified with --fold')) |
|
3220 revs = ['.'] |
|
3221 |
|
3222 wlock = lock = None |
|
3223 try: |
|
3224 wlock = repo.wlock() |
|
3225 lock = repo.lock() |
|
3226 |
|
3227 revs = scmutil.revrange(repo, revs) |
|
3228 if not opts['fold'] and len(revs) > 1: |
|
3229 # TODO: handle multiple revisions. This is somewhat tricky because |
|
3230 # if we want to edit a series of commits: |
|
3231 # |
|
3232 # a ---- b ---- c |
|
3233 # |
|
3234 # we need to rewrite a first, then directly rewrite b on top of the |
|
3235 # new a, then rewrite c on top of the new b. So we need to handle |
|
3236 # revisions in topological order. |
|
3237 raise error.Abort(_('editing multiple revisions without --fold is ' |
|
3238 'not currently supported')) |
|
3239 |
|
3240 if opts['fold']: |
|
3241 root, head = _foldcheck(repo, revs) |
|
3242 else: |
|
3243 if repo.revs("%ld and public()", revs): |
|
3244 raise error.Abort(_('cannot edit commit information for public ' |
|
3245 'revisions')) |
|
3246 newunstable = _disallowednewunstable(repo, revs) |
|
3247 if newunstable: |
|
3248 raise error.Abort( |
|
3249 _('cannot edit commit information in the middle of a '\ |
|
3250 'stack'), hint=_('%s will become unstable and new unstable'\ |
|
3251 ' changes are not allowed') % repo[newunstable.first()]) |
|
3252 root = head = repo[revs.first()] |
|
3253 |
|
3254 wctx = repo[None] |
|
3255 p1 = wctx.p1() |
|
3256 tr = repo.transaction('metaedit') |
|
3257 newp1 = None |
|
3258 try: |
|
3259 commitopts = opts.copy() |
|
3260 allctx = [repo[r] for r in revs] |
|
3261 targetphase = max(c.phase() for c in allctx) |
|
3262 |
|
3263 if commitopts.get('message') or commitopts.get('logfile'): |
|
3264 commitopts['edit'] = False |
|
3265 else: |
|
3266 if opts['fold']: |
|
3267 msgs = ["HG: This is a fold of %d changesets." % len(allctx)] |
|
3268 msgs += ["HG: Commit message of changeset %s.\n\n%s\n" % |
|
3269 (c.rev(), c.description()) for c in allctx] |
|
3270 else: |
|
3271 msgs = [head.description()] |
|
3272 commitopts['message'] = "\n".join(msgs) |
|
3273 commitopts['edit'] = True |
|
3274 |
|
3275 # TODO: if the author and message are the same, don't create a new |
|
3276 # hash. Right now we create a new hash because the date can be |
|
3277 # different. |
|
3278 newid, created = rewrite(repo, root, allctx, head, |
|
3279 [root.p1().node(), root.p2().node()], |
|
3280 commitopts=commitopts) |
|
3281 if created: |
|
3282 if p1.rev() in revs: |
|
3283 newp1 = newid |
|
3284 phases.retractboundary(repo, tr, targetphase, [newid]) |
|
3285 obsolete.createmarkers(repo, [(ctx, (repo[newid],)) |
|
3286 for ctx in allctx]) |
|
3287 else: |
|
3288 ui.status(_("nothing changed\n")) |
|
3289 tr.close() |
|
3290 finally: |
|
3291 tr.release() |
|
3292 |
|
3293 if opts['fold']: |
|
3294 ui.status('%i changesets folded\n' % len(revs)) |
|
3295 if newp1 is not None: |
|
3296 hg.update(repo, newp1) |
|
3297 finally: |
|
3298 lockmod.release(lock, wlock) |
|
3299 |
|
3300 def _foldcheck(repo, revs): |
|
3301 roots = repo.revs('roots(%ld)', revs) |
|
3302 if len(roots) > 1: |
|
3303 raise error.Abort(_("cannot fold non-linear revisions " |
|
3304 "(multiple roots given)")) |
|
3305 root = repo[roots.first()] |
|
3306 if root.phase() <= phases.public: |
|
3307 raise error.Abort(_("cannot fold public revisions")) |
|
3308 heads = repo.revs('heads(%ld)', revs) |
|
3309 if len(heads) > 1: |
|
3310 raise error.Abort(_("cannot fold non-linear revisions " |
|
3311 "(multiple heads given)")) |
|
3312 head = repo[heads.first()] |
|
3313 if _disallowednewunstable(repo, revs): |
|
3314 raise error.Abort(_("cannot fold chain not ending with a head "\ |
|
3315 "or with branching"), hint = _("new unstable"\ |
|
3316 " changesets are not allowed")) |
|
3317 return root, head |
|
3318 |
|
3319 def _disallowednewunstable(repo, revs): |
|
3320 allowunstable = obsolete.isenabled(repo, obsolete.allowunstableopt) |
|
3321 if allowunstable: |
|
3322 return revset.baseset() |
|
3323 return repo.revs("(%ld::) - %ld", revs, revs) |
|
3324 |
|
3325 @eh.wrapcommand('graft') |
|
3326 def graftwrapper(orig, ui, repo, *revs, **kwargs): |
|
3327 kwargs = dict(kwargs) |
|
3328 revs = list(revs) + kwargs.get('rev', []) |
|
3329 kwargs['rev'] = [] |
|
3330 obsoleted = kwargs.setdefault('obsolete', []) |
|
3331 |
|
3332 wlock = lock = None |
|
3333 try: |
|
3334 wlock = repo.wlock() |
|
3335 lock = repo.lock() |
|
3336 if kwargs.get('old_obsolete'): |
|
3337 if kwargs.get('continue'): |
|
3338 obsoleted.extend(repo.opener.read('graftstate').splitlines()) |
|
3339 else: |
|
3340 obsoleted.extend(revs) |
|
3341 # convert obsolete target into revs to avoid alias joke |
|
3342 obsoleted[:] = [str(i) for i in repo.revs('%lr', obsoleted)] |
|
3343 if obsoleted and len(revs) > 1: |
|
3344 |
|
3345 raise error.Abort(_('cannot graft multiple revisions while ' |
|
3346 'obsoleting (for now).')) |
|
3347 |
|
3348 return commitwrapper(orig, ui, repo,*revs, **kwargs) |
|
3349 finally: |
|
3350 lockmod.release(lock, wlock) |
|
3351 |
|
3352 @eh.extsetup |
|
3353 def oldevolveextsetup(ui): |
|
3354 for cmd in ['prune', 'uncommit', 'touch', 'fold']: |
|
3355 try: |
|
3356 entry = extensions.wrapcommand(cmdtable, cmd, |
|
3357 warnobserrors) |
|
3358 except error.UnknownCommand: |
|
3359 # Commands may be disabled |
|
3360 continue |
|
3361 |
|
3362 entry = cmdutil.findcmd('commit', commands.table)[1] |
|
3363 entry[1].append(('o', 'obsolete', [], |
|
3364 _("make commit obsolete this revision (DEPRECATED)"))) |
|
3365 entry = cmdutil.findcmd('graft', commands.table)[1] |
|
3366 entry[1].append(('o', 'obsolete', [], |
|
3367 _("make graft obsoletes this revision (DEPRECATED)"))) |
|
3368 entry[1].append(('O', 'old-obsolete', False, |
|
3369 _("make graft obsoletes its source (DEPRECATED)"))) |
|
3370 |
|
3371 ##################################################################### |
|
3372 ### Obsolescence marker exchange experimenation ### |
|
3373 ##################################################################### |
|
3374 |
|
3375 def obsexcmsg(ui, message, important=False): |
|
3376 verbose = ui.configbool('experimental', 'verbose-obsolescence-exchange', |
|
3377 False) |
|
3378 if verbose: |
|
3379 message = 'OBSEXC: ' + message |
|
3380 if important or verbose: |
|
3381 ui.status(message) |
|
3382 |
|
3383 def obsexcprg(ui, *args, **kwargs): |
|
3384 topic = 'obsmarkers exchange' |
|
3385 if ui.configbool('experimental', 'verbose-obsolescence-exchange', False): |
|
3386 topic = 'OBSEXC' |
|
3387 ui.progress(topic, *args, **kwargs) |
|
3388 |
|
3389 @eh.wrapfunction(exchange, '_pushdiscoveryobsmarkers') |
|
3390 def _pushdiscoveryobsmarkers(orig, pushop): |
|
3391 if (obsolete.isenabled(pushop.repo, obsolete.exchangeopt) |
|
3392 and pushop.repo.obsstore |
|
3393 and 'obsolete' in pushop.remote.listkeys('namespaces')): |
|
3394 repo = pushop.repo |
|
3395 obsexcmsg(repo.ui, "computing relevant nodes\n") |
|
3396 revs = list(repo.revs('::%ln', pushop.futureheads)) |
|
3397 unfi = repo.unfiltered() |
|
3398 cl = unfi.changelog |
|
3399 if not pushop.remote.capable('_evoext_obshash_0'): |
|
3400 # do not trust core yet |
|
3401 # return orig(pushop) |
|
3402 nodes = [cl.node(r) for r in revs] |
|
3403 if nodes: |
|
3404 obsexcmsg(repo.ui, "computing markers relevant to %i nodes\n" |
|
3405 % len(nodes)) |
|
3406 pushop.outobsmarkers = repo.obsstore.relevantmarkers(nodes) |
|
3407 else: |
|
3408 obsexcmsg(repo.ui, "markers already in sync\n") |
|
3409 pushop.outobsmarkers = [] |
|
3410 pushop.outobsmarkers = repo.obsstore.relevantmarkers(nodes) |
|
3411 return |
|
3412 |
|
3413 common = [] |
|
3414 obsexcmsg(repo.ui, "looking for common markers in %i nodes\n" |
|
3415 % len(revs)) |
|
3416 commonrevs = list(unfi.revs('::%ln', pushop.outgoing.commonheads)) |
|
3417 common = findcommonobsmarkers(pushop.ui, unfi, pushop.remote, |
|
3418 commonrevs) |
|
3419 |
|
3420 revs = list(unfi.revs('%ld - (::%ln)', revs, common)) |
|
3421 nodes = [cl.node(r) for r in revs] |
|
3422 if nodes: |
|
3423 obsexcmsg(repo.ui, "computing markers relevant to %i nodes\n" |
|
3424 % len(nodes)) |
|
3425 pushop.outobsmarkers = repo.obsstore.relevantmarkers(nodes) |
|
3426 else: |
|
3427 obsexcmsg(repo.ui, "markers already in sync\n") |
|
3428 pushop.outobsmarkers = [] |
|
3429 |
|
3430 @eh.wrapfunction(wireproto, 'capabilities') |
|
3431 def discocapabilities(orig, repo, proto): |
|
3432 """wrapper to advertise new capability""" |
|
3433 caps = orig(repo, proto) |
|
3434 if obsolete.isenabled(repo, obsolete.exchangeopt): |
|
3435 caps += ' _evoext_obshash_0' |
|
3436 return caps |
|
3437 |
|
3438 @eh.extsetup |
|
3439 def _installobsmarkersdiscovery(ui): |
|
3440 hgweb_mod.perms['evoext_obshash'] = 'pull' |
|
3441 hgweb_mod.perms['evoext_obshash1'] = 'pull' |
|
3442 # wrap command content |
|
3443 oldcap, args = wireproto.commands['capabilities'] |
|
3444 def newcap(repo, proto): |
|
3445 return discocapabilities(oldcap, repo, proto) |
|
3446 wireproto.commands['capabilities'] = (newcap, args) |
|
3447 wireproto.commands['evoext_obshash'] = (srv_obshash, 'nodes') |
|
3448 wireproto.commands['evoext_obshash1'] = (srv_obshash1, 'nodes') |
|
3449 if getattr(exchange, '_pushdiscoveryobsmarkers', None) is None: |
|
3450 ui.warn(_('evolve: your mercurial version is too old\n' |
|
3451 'evolve: (running in degraded mode, push will ' |
|
3452 'includes all markers)\n')) |
|
3453 else: |
|
3454 olddisco = exchange.pushdiscoverymapping['obsmarker'] |
|
3455 def newdisco(pushop): |
|
3456 _pushdiscoveryobsmarkers(olddisco, pushop) |
|
3457 exchange.pushdiscoverymapping['obsmarker'] = newdisco |
|
3458 |
|
3459 ### Set discovery START |
|
3460 |
|
3461 from mercurial import dagutil |
|
3462 from mercurial import setdiscovery |
|
3463 |
|
3464 def _obshash(repo, nodes, version=0): |
|
3465 if version == 0: |
|
3466 hashs = _obsrelsethashtreefm0(repo) |
|
3467 elif version ==1: |
|
3468 hashs = _obsrelsethashtreefm1(repo) |
|
3469 else: |
|
3470 assert False |
|
3471 nm = repo.changelog.nodemap |
|
3472 revs = [nm.get(n) for n in nodes] |
|
3473 return [r is None and nullid or hashs[r][1] for r in revs] |
|
3474 |
|
3475 def srv_obshash(repo, proto, nodes): |
|
3476 return wireproto.encodelist(_obshash(repo, wireproto.decodelist(nodes))) |
|
3477 |
|
3478 def srv_obshash1(repo, proto, nodes): |
|
3479 return wireproto.encodelist(_obshash(repo, wireproto.decodelist(nodes), |
|
3480 version=1)) |
|
3481 |
|
3482 @eh.addattr(localrepo.localpeer, 'evoext_obshash') |
|
3483 def local_obshash(peer, nodes): |
|
3484 return _obshash(peer._repo, nodes) |
|
3485 |
|
3486 @eh.addattr(localrepo.localpeer, 'evoext_obshash1') |
|
3487 def local_obshash1(peer, nodes): |
|
3488 return _obshash(peer._repo, nodes, version=1) |
|
3489 |
|
3490 @eh.addattr(wireproto.wirepeer, 'evoext_obshash') |
|
3491 def peer_obshash(self, nodes): |
|
3492 d = self._call("evoext_obshash", nodes=wireproto.encodelist(nodes)) |
|
3493 try: |
|
3494 return wireproto.decodelist(d) |
|
3495 except ValueError: |
|
3496 self._abort(error.ResponseError(_("unexpected response:"), d)) |
|
3497 |
|
3498 @eh.addattr(wireproto.wirepeer, 'evoext_obshash1') |
|
3499 def peer_obshash1(self, nodes): |
|
3500 d = self._call("evoext_obshash1", nodes=wireproto.encodelist(nodes)) |
|
3501 try: |
|
3502 return wireproto.decodelist(d) |
|
3503 except ValueError: |
|
3504 self._abort(error.ResponseError(_("unexpected response:"), d)) |
|
3505 |
|
3506 def findcommonobsmarkers(ui, local, remote, probeset, |
|
3507 initialsamplesize=100, |
|
3508 fullsamplesize=200): |
|
3509 # from discovery |
|
3510 roundtrips = 0 |
|
3511 cl = local.changelog |
|
3512 dag = dagutil.revlogdag(cl) |
|
3513 missing = set() |
|
3514 common = set() |
|
3515 undecided = set(probeset) |
|
3516 totalnb = len(undecided) |
|
3517 ui.progress(_("comparing with other"), 0, total=totalnb) |
|
3518 _takefullsample = setdiscovery._takefullsample |
|
3519 if remote.capable('_evoext_obshash_1'): |
|
3520 getremotehash = remote.evoext_obshash1 |
|
3521 localhash = _obsrelsethashtreefm1(local) |
|
3522 else: |
|
3523 getremotehash = remote.evoext_obshash |
|
3524 localhash = _obsrelsethashtreefm0(local) |
|
3525 |
|
3526 while undecided: |
|
3527 |
|
3528 ui.note(_("sampling from both directions\n")) |
|
3529 if len(undecided) < fullsamplesize: |
|
3530 sample = set(undecided) |
|
3531 else: |
|
3532 sample = _takefullsample(dag, undecided, size=fullsamplesize) |
|
3533 |
|
3534 roundtrips += 1 |
|
3535 ui.progress(_("comparing with other"), totalnb - len(undecided), |
|
3536 total=totalnb) |
|
3537 ui.debug("query %i; still undecided: %i, sample size is: %i\n" |
|
3538 % (roundtrips, len(undecided), len(sample))) |
|
3539 # indices between sample and externalized version must match |
|
3540 sample = list(sample) |
|
3541 remotehash = getremotehash(dag.externalizeall(sample)) |
|
3542 |
|
3543 yesno = [localhash[ix][1] == remotehash[si] |
|
3544 for si, ix in enumerate(sample)] |
|
3545 |
|
3546 commoninsample = set(n for i, n in enumerate(sample) if yesno[i]) |
|
3547 common.update(dag.ancestorset(commoninsample, common)) |
|
3548 |
|
3549 missinginsample = [n for i, n in enumerate(sample) if not yesno[i]] |
|
3550 missing.update(dag.descendantset(missinginsample, missing)) |
|
3551 |
|
3552 undecided.difference_update(missing) |
|
3553 undecided.difference_update(common) |
|
3554 |
|
3555 |
|
3556 ui.progress(_("comparing with other"), None) |
|
3557 result = dag.headsetofconnecteds(common) |
|
3558 ui.debug("%d total queries\n" % roundtrips) |
|
3559 |
|
3560 if not result: |
|
3561 return set([nullid]) |
|
3562 return dag.externalizeall(result) |
|
3563 |
|
3564 |
|
3565 _pushkeyescape = getattr(obsolete, '_pushkeyescape', None) |
|
3566 |
|
3567 class pushobsmarkerStringIO(StringIO): |
|
3568 """hacky string io for progress""" |
|
3569 |
|
3570 @util.propertycache |
|
3571 def length(self): |
|
3572 return len(self.getvalue()) |
|
3573 |
|
3574 def read(self, size=None): |
|
3575 obsexcprg(self.ui, self.tell(), unit=_("bytes"), total=self.length) |
|
3576 return StringIO.read(self, size) |
|
3577 |
|
3578 def __iter__(self): |
|
3579 d = self.read(4096) |
|
3580 while d: |
|
3581 yield d |
|
3582 d = self.read(4096) |
|
3583 |
|
3584 @eh.wrapfunction(exchange, '_pushobsolete') |
|
3585 def _pushobsolete(orig, pushop): |
|
3586 """utility function to push obsolete markers to a remote""" |
|
3587 stepsdone = getattr(pushop, 'stepsdone', None) |
|
3588 if stepsdone is not None: |
|
3589 if 'obsmarkers' in stepsdone: |
|
3590 return |
|
3591 stepsdone.add('obsmarkers') |
|
3592 if util.safehasattr(pushop, 'cgresult'): |
|
3593 cgresult = pushop.cgresult |
|
3594 else: |
|
3595 cgresult = pushop.ret |
|
3596 if cgresult == 0: |
|
3597 return |
|
3598 pushop.ui.debug('try to push obsolete markers to remote\n') |
|
3599 repo = pushop.repo |
|
3600 remote = pushop.remote |
|
3601 if (obsolete.isenabled(repo, obsolete.exchangeopt) and repo.obsstore and |
|
3602 'obsolete' in remote.listkeys('namespaces')): |
|
3603 markers = pushop.outobsmarkers |
|
3604 if not markers: |
|
3605 obsexcmsg(repo.ui, "no marker to push\n") |
|
3606 elif remote.capable('_evoext_pushobsmarkers_0'): |
|
3607 obsdata = pushobsmarkerStringIO() |
|
3608 for chunk in obsolete.encodemarkers(markers, True): |
|
3609 obsdata.write(chunk) |
|
3610 obsdata.seek(0) |
|
3611 obsdata.ui = repo.ui |
|
3612 obsexcmsg(repo.ui, "pushing %i obsolescence markers (%i bytes)\n" |
|
3613 % (len(markers), len(obsdata.getvalue())), |
|
3614 True) |
|
3615 remote.evoext_pushobsmarkers_0(obsdata) |
|
3616 obsexcprg(repo.ui, None) |
|
3617 else: |
|
3618 rslts = [] |
|
3619 remotedata = _pushkeyescape(markers).items() |
|
3620 totalbytes = sum(len(d) for k, d in remotedata) |
|
3621 sentbytes = 0 |
|
3622 obsexcmsg(repo.ui, "pushing %i obsolescence markers in %i " |
|
3623 "pushkey payload (%i bytes)\n" |
|
3624 % (len(markers), len(remotedata), totalbytes), |
|
3625 True) |
|
3626 for key, data in remotedata: |
|
3627 obsexcprg(repo.ui, sentbytes, item=key, unit=_("bytes"), |
|
3628 total=totalbytes) |
|
3629 rslts.append(remote.pushkey('obsolete', key, '', data)) |
|
3630 sentbytes += len(data) |
|
3631 obsexcprg(repo.ui, sentbytes, item=key, unit=_("bytes"), |
|
3632 total=totalbytes) |
|
3633 obsexcprg(repo.ui, None) |
|
3634 if [r for r in rslts if not r]: |
|
3635 msg = _('failed to push some obsolete markers!\n') |
|
3636 repo.ui.warn(msg) |
|
3637 obsexcmsg(repo.ui, "DONE\n") |
|
3638 |
|
3639 |
|
3640 @eh.addattr(wireproto.wirepeer, 'evoext_pushobsmarkers_0') |
|
3641 def client_pushobsmarkers(self, obsfile): |
|
3642 """wireprotocol peer method""" |
|
3643 self.requirecap('_evoext_pushobsmarkers_0', |
|
3644 _('push obsolete markers faster')) |
|
3645 ret, output = self._callpush('evoext_pushobsmarkers_0', obsfile) |
|
3646 for l in output.splitlines(True): |
|
3647 self.ui.status(_('remote: '), l) |
|
3648 return ret |
|
3649 |
|
3650 @eh.addattr(httppeer.httppeer, 'evoext_pushobsmarkers_0') |
|
3651 def httpclient_pushobsmarkers(self, obsfile): |
|
3652 """httpprotocol peer method |
|
3653 (Cannot simply use _callpush as http is doing some special handling)""" |
|
3654 self.requirecap('_evoext_pushobsmarkers_0', |
|
3655 _('push obsolete markers faster')) |
|
3656 try: |
|
3657 r = self._call('evoext_pushobsmarkers_0', data=obsfile) |
|
3658 vals = r.split('\n', 1) |
|
3659 if len(vals) < 2: |
|
3660 raise error.ResponseError(_("unexpected response:"), r) |
|
3661 |
|
3662 for l in vals[1].splitlines(True): |
|
3663 if l.strip(): |
|
3664 self.ui.status(_('remote: '), l) |
|
3665 return vals[0] |
|
3666 except socket.error as err: |
|
3667 if err.args[0] in (errno.ECONNRESET, errno.EPIPE): |
|
3668 raise error.Abort(_('push failed: %s') % err.args[1]) |
|
3669 raise error.Abort(err.args[1]) |
|
3670 |
|
3671 @eh.wrapfunction(localrepo.localrepository, '_restrictcapabilities') |
|
3672 def local_pushobsmarker_capabilities(orig, repo, caps): |
|
3673 caps = orig(repo, caps) |
|
3674 caps.add('_evoext_pushobsmarkers_0') |
|
3675 return caps |
|
3676 |
|
3677 def _pushobsmarkers(repo, data): |
|
3678 tr = lock = None |
|
3679 try: |
|
3680 lock = repo.lock() |
|
3681 tr = repo.transaction('pushkey: obsolete markers') |
|
3682 new = repo.obsstore.mergemarkers(tr, data) |
|
3683 if new is not None: |
|
3684 obsexcmsg(repo.ui, "%i obsolescence markers added\n" % new, True) |
|
3685 tr.close() |
|
3686 finally: |
|
3687 lockmod.release(tr, lock) |
|
3688 repo.hook('evolve_pushobsmarkers') |
|
3689 |
|
3690 @eh.addattr(localrepo.localpeer, 'evoext_pushobsmarkers_0') |
|
3691 def local_pushobsmarkers(peer, obsfile): |
|
3692 data = obsfile.read() |
|
3693 _pushobsmarkers(peer._repo, data) |
|
3694 |
|
3695 def srv_pushobsmarkers(repo, proto): |
|
3696 """wireprotocol command""" |
|
3697 fp = StringIO() |
|
3698 proto.redirect() |
|
3699 proto.getfile(fp) |
|
3700 data = fp.getvalue() |
|
3701 fp.close() |
|
3702 _pushobsmarkers(repo, data) |
|
3703 return wireproto.pushres(0) |
|
3704 |
|
3705 def _buildpullobsmarkersboundaries(pullop): |
|
3706 """small funtion returning the argument for pull markers call |
|
3707 may to contains 'heads' and 'common'. skip the key for None. |
|
3708 |
|
3709 Its a separed functio to play around with strategy for that.""" |
|
3710 repo = pullop.repo |
|
3711 remote = pullop.remote |
|
3712 unfi = repo.unfiltered() |
|
3713 revs = unfi.revs('::(%ln - null)', pullop.common) |
|
3714 common = [nullid] |
|
3715 if remote.capable('_evoext_obshash_0'): |
|
3716 obsexcmsg(repo.ui, "looking for common markers in %i nodes\n" |
|
3717 % len(revs)) |
|
3718 common = findcommonobsmarkers(repo.ui, repo, remote, revs) |
|
3719 return {'heads': pullop.pulledsubset, 'common': common} |
|
3720 |
|
3721 @eh.uisetup |
|
3722 def addgetbundleargs(self): |
|
3723 gboptsmap['evo_obscommon'] = 'nodes' |
|
3724 |
|
3725 @eh.wrapfunction(exchange, '_pullbundle2extraprepare') |
|
3726 def _addobscommontob2pull(orig, pullop, kwargs): |
|
3727 ret = orig(pullop, kwargs) |
|
3728 if ('obsmarkers' in kwargs and |
|
3729 pullop.remote.capable('_evoext_getbundle_obscommon')): |
|
3730 boundaries = _buildpullobsmarkersboundaries(pullop) |
|
3731 common = boundaries['common'] |
|
3732 if common != [nullid]: |
|
3733 kwargs['evo_obscommon'] = common |
|
3734 return ret |
|
3735 |
|
3736 @eh.wrapfunction(exchange, '_getbundleobsmarkerpart') |
|
3737 def _getbundleobsmarkerpart(orig, bundler, repo, source, **kwargs): |
|
3738 if 'evo_obscommon' not in kwargs: |
|
3739 return orig(bundler, repo, source, **kwargs) |
|
3740 |
|
3741 heads = kwargs.get('heads') |
|
3742 if kwargs.get('obsmarkers', False): |
|
3743 if heads is None: |
|
3744 heads = repo.heads() |
|
3745 obscommon = kwargs.get('evo_obscommon', ()) |
|
3746 assert obscommon |
|
3747 obsset = repo.unfiltered().set('::%ln - ::%ln', heads, obscommon) |
|
3748 subset = [c.node() for c in obsset] |
|
3749 markers = repo.obsstore.relevantmarkers(subset) |
|
3750 exchange.buildobsmarkerspart(bundler, markers) |
|
3751 |
|
3752 @eh.uisetup |
|
3753 def installgetbundlepartgen(ui): |
|
3754 origfunc = exchange.getbundle2partsmapping['obsmarkers'] |
|
3755 def newfunc(*args, **kwargs): |
|
3756 return _getbundleobsmarkerpart(origfunc, *args, **kwargs) |
|
3757 exchange.getbundle2partsmapping['obsmarkers'] = newfunc |
|
3758 |
|
3759 @eh.wrapfunction(exchange, '_pullobsolete') |
|
3760 def _pullobsolete(orig, pullop): |
|
3761 if not obsolete.isenabled(pullop.repo, obsolete.exchangeopt): |
|
3762 return None |
|
3763 if 'obsmarkers' not in getattr(pullop, 'todosteps', ['obsmarkers']): |
|
3764 return None |
|
3765 if 'obsmarkers' in getattr(pullop, 'stepsdone', []): |
|
3766 return None |
|
3767 wirepull = pullop.remote.capable('_evoext_pullobsmarkers_0') |
|
3768 if not wirepull: |
|
3769 return orig(pullop) |
|
3770 if 'obsolete' not in pullop.remote.listkeys('namespaces'): |
|
3771 return None # remote opted out of obsolescence marker exchange |
|
3772 tr = None |
|
3773 ui = pullop.repo.ui |
|
3774 boundaries = _buildpullobsmarkersboundaries(pullop) |
|
3775 if not set(boundaries['heads']) - set(boundaries['common']): |
|
3776 obsexcmsg(ui, "nothing to pull\n") |
|
3777 return None |
|
3778 |
|
3779 obsexcmsg(ui, "pull obsolescence markers\n", True) |
|
3780 new = 0 |
|
3781 |
|
3782 if wirepull: |
|
3783 obsdata = pullop.remote.evoext_pullobsmarkers_0(**boundaries) |
|
3784 obsdata = obsdata.read() |
|
3785 if len(obsdata) > 5: |
|
3786 obsexcmsg(ui, "merging obsolescence markers (%i bytes)\n" |
|
3787 % len(obsdata)) |
|
3788 tr = pullop.gettransaction() |
|
3789 old = len(pullop.repo.obsstore._all) |
|
3790 pullop.repo.obsstore.mergemarkers(tr, obsdata) |
|
3791 new = len(pullop.repo.obsstore._all) - old |
|
3792 obsexcmsg(ui, "%i obsolescence markers added\n" % new, True) |
|
3793 else: |
|
3794 obsexcmsg(ui, "no unknown remote markers\n") |
|
3795 obsexcmsg(ui, "DONE\n") |
|
3796 if new: |
|
3797 pullop.repo.invalidatevolatilesets() |
|
3798 return tr |
|
3799 |
|
3800 def _getobsmarkersstream(repo, heads=None, common=None): |
|
3801 revset = '' |
|
3802 args = [] |
|
3803 repo = repo.unfiltered() |
|
3804 if heads is None: |
|
3805 revset = 'all()' |
|
3806 elif heads: |
|
3807 revset += "(::%ln)" |
|
3808 args.append(heads) |
|
3809 else: |
|
3810 assert False, 'pulling no heads?' |
|
3811 if common: |
|
3812 revset += ' - (::%ln)' |
|
3813 args.append(common) |
|
3814 nodes = [c.node() for c in repo.set(revset, *args)] |
|
3815 markers = repo.obsstore.relevantmarkers(nodes) |
|
3816 obsdata = StringIO() |
|
3817 for chunk in obsolete.encodemarkers(markers, True): |
|
3818 obsdata.write(chunk) |
|
3819 obsdata.seek(0) |
|
3820 return obsdata |
|
3821 |
|
3822 @eh.addattr(wireproto.wirepeer, 'evoext_pullobsmarkers_0') |
|
3823 def client_pullobsmarkers(self, heads=None, common=None): |
|
3824 self.requirecap('_evoext_pullobsmarkers_0', _('look up remote obsmarkers')) |
|
3825 opts = {} |
|
3826 if heads is not None: |
|
3827 opts['heads'] = wireproto.encodelist(heads) |
|
3828 if common is not None: |
|
3829 opts['common'] = wireproto.encodelist(common) |
|
3830 if util.safehasattr(self, '_callcompressable'): |
|
3831 f = self._callcompressable("evoext_pullobsmarkers_0", **opts) |
|
3832 else: |
|
3833 f = self._callstream("evoext_pullobsmarkers_0", **opts) |
|
3834 f = self._decompress(f) |
|
3835 length = int(f.read(20)) |
|
3836 chunk = 4096 |
|
3837 current = 0 |
|
3838 data = StringIO() |
|
3839 ui = self.ui |
|
3840 obsexcprg(ui, current, unit=_("bytes"), total=length) |
|
3841 while current < length: |
|
3842 readsize = min(length - current, chunk) |
|
3843 data.write(f.read(readsize)) |
|
3844 current += readsize |
|
3845 obsexcprg(ui, current, unit=_("bytes"), total=length) |
|
3846 obsexcprg(ui, None) |
|
3847 data.seek(0) |
|
3848 return data |
|
3849 |
|
3850 @eh.addattr(localrepo.localpeer, 'evoext_pullobsmarkers_0') |
|
3851 def local_pullobsmarkers(self, heads=None, common=None): |
|
3852 return _getobsmarkersstream(self._repo, heads=heads, common=common) |
|
3853 |
|
3854 # The wireproto.streamres API changed, handling chunking and compression |
|
3855 # directly. Handle either case. |
|
3856 if util.safehasattr(wireproto.abstractserverproto, 'groupchunks'): |
|
3857 # We need to handle chunking and compression directly |
|
3858 def streamres(d, proto): |
|
3859 return wireproto.streamres(proto.groupchunks(d)) |
|
3860 else: |
|
3861 # Leave chunking and compression to streamres |
|
3862 def streamres(d, proto): |
|
3863 return wireproto.streamres(reader=d, v1compressible=True) |
|
3864 |
|
3865 def srv_pullobsmarkers(repo, proto, others): |
|
3866 opts = wireproto.options('', ['heads', 'common'], others) |
|
3867 for k, v in opts.iteritems(): |
|
3868 if k in ('heads', 'common'): |
|
3869 opts[k] = wireproto.decodelist(v) |
|
3870 obsdata = _getobsmarkersstream(repo, **opts) |
|
3871 finaldata = StringIO() |
|
3872 obsdata = obsdata.getvalue() |
|
3873 finaldata.write('%20i' % len(obsdata)) |
|
3874 finaldata.write(obsdata) |
|
3875 finaldata.seek(0) |
|
3876 return streamres(finaldata, proto) |
|
3877 |
|
3878 def _obsrelsethashtreefm0(repo): |
|
3879 return _obsrelsethashtree(repo, obsolete._fm0encodeonemarker) |
|
3880 |
|
3881 def _obsrelsethashtreefm1(repo): |
|
3882 return _obsrelsethashtree(repo, obsolete._fm1encodeonemarker) |
|
3883 |
|
3884 def _obsrelsethashtree(repo, encodeonemarker): |
|
3885 cache = [] |
|
3886 unfi = repo.unfiltered() |
|
3887 markercache = {} |
|
3888 repo.ui.progress(_("preparing locally"), 0, total=len(unfi)) |
|
3889 for i in unfi: |
|
3890 ctx = unfi[i] |
|
3891 entry = 0 |
|
3892 sha = hashlib.sha1() |
|
3893 # add data from p1 |
|
3894 for p in ctx.parents(): |
|
3895 p = p.rev() |
|
3896 if p < 0: |
|
3897 p = nullid |
|
3898 else: |
|
3899 p = cache[p][1] |
|
3900 if p != nullid: |
|
3901 entry += 1 |
|
3902 sha.update(p) |
|
3903 tmarkers = repo.obsstore.relevantmarkers([ctx.node()]) |
|
3904 if tmarkers: |
|
3905 bmarkers = [] |
|
3906 for m in tmarkers: |
|
3907 if not m in markercache: |
|
3908 markercache[m] = encodeonemarker(m) |
|
3909 bmarkers.append(markercache[m]) |
|
3910 bmarkers.sort() |
|
3911 for m in bmarkers: |
|
3912 entry += 1 |
|
3913 sha.update(m) |
|
3914 if entry: |
|
3915 cache.append((ctx.node(), sha.digest())) |
|
3916 else: |
|
3917 cache.append((ctx.node(), nullid)) |
|
3918 repo.ui.progress(_("preparing locally"), i, total=len(unfi)) |
|
3919 repo.ui.progress(_("preparing locally"), None) |
|
3920 return cache |
|
3921 |
|
3922 @command('debugobsrelsethashtree', |
|
3923 [('', 'v0', None, 'hash on marker format "0"'), |
|
3924 ('', 'v1', None, 'hash on marker format "1" (default)')] , _('')) |
|
3925 def debugobsrelsethashtree(ui, repo, v0=False, v1=False): |
|
3926 """display Obsolete markers, Relevant Set, Hash Tree |
|
3927 changeset-node obsrelsethashtree-node |
|
3928 |
|
3929 It computed form the "orsht" of its parent and markers |
|
3930 relevant to the changeset itself.""" |
|
3931 if v0 and v1: |
|
3932 raise error.Abort('cannot only specify one format') |
|
3933 elif v0: |
|
3934 treefunc = _obsrelsethashtreefm0 |
|
3935 else: |
|
3936 treefunc = _obsrelsethashtreefm1 |
|
3937 |
|
3938 for chg, obs in treefunc(repo): |
|
3939 ui.status('%s %s\n' % (node.hex(chg), node.hex(obs))) |
|
3940 |
|
3941 _bestformat = max(obsolete.formats.keys()) |
|
3942 |
|
3943 |
|
3944 @eh.wrapfunction(obsolete, '_checkinvalidmarkers') |
|
3945 def _checkinvalidmarkers(orig, markers): |
|
3946 """search for marker with invalid data and raise error if needed |
|
3947 |
|
3948 Exist as a separated function to allow the evolve extension for a more |
|
3949 subtle handling. |
|
3950 """ |
|
3951 if 'debugobsconvert' in sys.argv: |
|
3952 return |
|
3953 for mark in markers: |
|
3954 if node.nullid in mark[1]: |
|
3955 raise error.Abort(_('bad obsolescence marker detected: ' |
|
3956 'invalid successors nullid'), |
|
3957 hint=_('You should run `hg debugobsconvert`')) |
|
3958 |
|
3959 @command( |
|
3960 'debugobsconvert', |
|
3961 [('', 'new-format', _bestformat, _('Destination format for markers.'))], |
|
3962 '') |
|
3963 def debugobsconvert(ui, repo, new_format): |
|
3964 origmarkers = repo.obsstore._all # settle version |
|
3965 if new_format == repo.obsstore._version: |
|
3966 msg = _('New format is the same as the old format, not upgrading!') |
|
3967 raise error.Abort(msg) |
|
3968 f = repo.svfs('obsstore', 'wb', atomictemp=True) |
|
3969 known = set() |
|
3970 markers = [] |
|
3971 for m in origmarkers: |
|
3972 # filter out invalid markers |
|
3973 if nullid in m[1]: |
|
3974 m = list(m) |
|
3975 m[1] = tuple(s for s in m[1] if s != nullid) |
|
3976 m = tuple(m) |
|
3977 if m in known: |
|
3978 continue |
|
3979 known.add(m) |
|
3980 markers.append(m) |
|
3981 ui.write(_('Old store is version %d, will rewrite in version %d\n') % ( |
|
3982 repo.obsstore._version, new_format)) |
|
3983 map(f.write, obsolete.encodemarkers(markers, True, new_format)) |
|
3984 f.close() |
|
3985 ui.write(_('Done!\n')) |
|
3986 |
|
3987 |
|
3988 @eh.wrapfunction(wireproto, 'capabilities') |
|
3989 def capabilities(orig, repo, proto): |
|
3990 """wrapper to advertise new capability""" |
|
3991 caps = orig(repo, proto) |
|
3992 if obsolete.isenabled(repo, obsolete.exchangeopt): |
|
3993 caps += ' _evoext_pushobsmarkers_0' |
|
3994 caps += ' _evoext_pullobsmarkers_0' |
|
3995 caps += ' _evoext_obshash_0' |
|
3996 caps += ' _evoext_obshash_1' |
|
3997 caps += ' _evoext_getbundle_obscommon' |
|
3998 return caps |
|
3999 |
|
4000 |
|
4001 @eh.extsetup |
|
4002 def _installwireprotocol(ui): |
|
4003 localrepo.moderncaps.add('_evoext_pullobsmarkers_0') |
|
4004 hgweb_mod.perms['evoext_pushobsmarkers_0'] = 'push' |
|
4005 hgweb_mod.perms['evoext_pullobsmarkers_0'] = 'pull' |
|
4006 wireproto.commands['evoext_pushobsmarkers_0'] = (srv_pushobsmarkers, '') |
|
4007 wireproto.commands['evoext_pullobsmarkers_0'] = (srv_pullobsmarkers, '*') |
|
4008 # wrap command content |
|
4009 oldcap, args = wireproto.commands['capabilities'] |
|
4010 def newcap(repo, proto): |
|
4011 return capabilities(oldcap, repo, proto) |
|
4012 wireproto.commands['capabilities'] = (newcap, args) |
|
4013 |
|
4014 # Mercurial >= 3.6 passes ui |
|
4015 def _helploader(ui=None): |
|
4016 return help.gettext(evolutionhelptext) |
|
4017 |
|
4018 @eh.uisetup |
|
4019 def _setuphelp(ui): |
|
4020 for entry in help.helptable: |
|
4021 if entry[0] == "evolution": |
|
4022 break |
|
4023 else: |
|
4024 help.helptable.append((["evolution"], _("Safely Rewriting History"), |
|
4025 _helploader)) |
|
4026 help.helptable.sort() |
|
4027 |
|
4028 def _relocatecommit(repo, orig, commitmsg): |
|
4029 if commitmsg is None: |
|
4030 commitmsg = orig.description() |
|
4031 extra = dict(orig.extra()) |
|
4032 if 'branch' in extra: |
|
4033 del extra['branch'] |
|
4034 extra['rebase_source'] = orig.hex() |
|
4035 |
|
4036 backup = repo.ui.backupconfig('phases', 'new-commit') |
|
4037 try: |
|
4038 targetphase = max(orig.phase(), phases.draft) |
|
4039 repo.ui.setconfig('phases', 'new-commit', targetphase, 'evolve') |
|
4040 # Commit might fail if unresolved files exist |
|
4041 nodenew = repo.commit(text=commitmsg, user=orig.user(), |
|
4042 date=orig.date(), extra=extra) |
|
4043 finally: |
|
4044 repo.ui.restoreconfig(backup) |
|
4045 return nodenew |
|
4046 |
|
4047 def _finalizerelocate(repo, orig, dest, nodenew, tr): |
|
4048 destbookmarks = repo.nodebookmarks(dest.node()) |
|
4049 nodesrc = orig.node() |
|
4050 destphase = repo[nodesrc].phase() |
|
4051 oldbookmarks = repo.nodebookmarks(nodesrc) |
|
4052 if nodenew is not None: |
|
4053 phases.retractboundary(repo, tr, destphase, [nodenew]) |
|
4054 obsolete.createmarkers(repo, [(repo[nodesrc], (repo[nodenew],))]) |
|
4055 for book in oldbookmarks: |
|
4056 repo._bookmarks[book] = nodenew |
|
4057 else: |
|
4058 obsolete.createmarkers(repo, [(repo[nodesrc], ())]) |
|
4059 # Behave like rebase, move bookmarks to dest |
|
4060 for book in oldbookmarks: |
|
4061 repo._bookmarks[book] = dest.node() |
|
4062 for book in destbookmarks: # restore bookmark that rebase move |
|
4063 repo._bookmarks[book] = dest.node() |
|
4064 if oldbookmarks or destbookmarks: |
|
4065 repo._bookmarks.recordchange(tr) |
|
4066 |
|
4067 evolvestateversion = 0 |
|
4068 |
|
4069 @eh.uisetup |
|
4070 def setupevolveunfinished(ui): |
|
4071 data = ('evolvestate', True, False, _('evolve in progress'), |
|
4072 _("use 'hg evolve --continue' or 'hg update -C .' to abort")) |
|
4073 cmdutil.unfinishedstates.append(data) |
|
4074 |
|
4075 @eh.wrapfunction(hg, 'clean') |
|
4076 def clean(orig, repo, *args, **kwargs): |
|
4077 ret = orig(repo, *args, **kwargs) |
|
4078 util.unlinkpath(repo.join('evolvestate'), ignoremissing=True) |
|
4079 return ret |
|
4080 |
|
4081 def _evolvestatewrite(repo, state): |
|
4082 # [version] |
|
4083 # [type][length][content] |
|
4084 # |
|
4085 # `version` is a 4 bytes integer (handled at higher level) |
|
4086 # `type` is a single character, `length` is a 4 byte integer, and |
|
4087 # `content` is an arbitrary byte sequence of length `length`. |
|
4088 f = repo.vfs('evolvestate', 'w') |
|
4089 try: |
|
4090 f.write(_pack('>I', evolvestateversion)) |
|
4091 current = state['current'] |
|
4092 key = 'C' # as in 'current' |
|
4093 format = '>sI%is' % len(current) |
|
4094 f.write(_pack(format, key, len(current), current)) |
|
4095 finally: |
|
4096 f.close() |
|
4097 |
|
4098 def _evolvestateread(repo): |
|
4099 try: |
|
4100 f = repo.vfs('evolvestate') |
|
4101 except IOError as err: |
|
4102 if err.errno != errno.ENOENT: |
|
4103 raise |
|
4104 return None |
|
4105 try: |
|
4106 versionblob = f.read(4) |
|
4107 if len(versionblob) < 4: |
|
4108 repo.ui.debug('ignoring corrupted evolvestte (file contains %i bits)' |
|
4109 % len(versionblob)) |
|
4110 return None |
|
4111 version = _unpack('>I', versionblob)[0] |
|
4112 if version != evolvestateversion: |
|
4113 raise error.Abort(_('unknown evolvestate version %i') |
|
4114 % version, hint=_('upgrade your evolve')) |
|
4115 records = [] |
|
4116 data = f.read() |
|
4117 off = 0 |
|
4118 end = len(data) |
|
4119 while off < end: |
|
4120 rtype = data[off] |
|
4121 off += 1 |
|
4122 length = _unpack('>I', data[off:(off + 4)])[0] |
|
4123 off += 4 |
|
4124 record = data[off:(off + length)] |
|
4125 off += length |
|
4126 if rtype == 't': |
|
4127 rtype, record = record[0], record[1:] |
|
4128 records.append((rtype, record)) |
|
4129 state = {} |
|
4130 for rtype, rdata in records: |
|
4131 if rtype == 'C': |
|
4132 state['current'] = rdata |
|
4133 elif rtype.lower(): |
|
4134 repo.ui.debug('ignore evolve state record type %s' % rtype) |
|
4135 else: |
|
4136 raise error.Abort(_('unknown evolvestate field type %r') |
|
4137 % rtype, hint=_('upgrade your evolve')) |
|
4138 return state |
|
4139 finally: |
|
4140 f.close() |
|
4141 |
|
4142 def _evolvestatedelete(repo): |
|
4143 util.unlinkpath(repo.join('evolvestate'), ignoremissing=True) |
|
4144 |
|
4145 def _evolvemerge(repo, orig, dest, pctx, keepbranch): |
|
4146 """Used by the evolve function to merge dest on top of pctx. |
|
4147 return the same tuple as merge.graft""" |
|
4148 if repo['.'].rev() != dest.rev(): |
|
4149 #assert False |
|
4150 try: |
|
4151 merge.update(repo, |
|
4152 dest, |
|
4153 branchmerge=False, |
|
4154 force=True) |
|
4155 except TypeError: |
|
4156 # Mercurial < 43c00ca887d1 (3.7) |
|
4157 merge.update(repo, |
|
4158 dest, |
|
4159 branchmerge=False, |
|
4160 force=True, |
|
4161 partial=False) |
|
4162 if bmactive(repo): |
|
4163 repo.ui.status(_("(leaving bookmark %s)\n") % bmactive(repo)) |
|
4164 bmdeactivate(repo) |
|
4165 if keepbranch: |
|
4166 repo.dirstate.setbranch(orig.branch()) |
|
4167 if util.safehasattr(repo, 'currenttopic'): |
|
4168 # uurrgs |
|
4169 # there no other topic setter yet |
|
4170 if not orig.topic() and repo.vfs.exists('topic'): |
|
4171 repo.vfs.unlink('topic') |
|
4172 else: |
|
4173 with repo.vfs.open('topic', 'w') as f: |
|
4174 f.write(orig.topic()) |
|
4175 |
|
4176 try: |
|
4177 r = merge.graft(repo, orig, pctx, ['local', 'graft'], True) |
|
4178 except TypeError: |
|
4179 # not using recent enough mercurial |
|
4180 if len(orig.parents()) == 2: |
|
4181 raise error.Abort( |
|
4182 _("no support for evolving merge changesets yet"), |
|
4183 hint=_("Redo the merge and use `hg prune <old> --succ " |
|
4184 "<new>` to obsolete the old one")) |
|
4185 |
|
4186 r = merge.graft(repo, orig, pctx, ['local', 'graft']) |
|
4187 return r |
|