25 from mercurial.commands import walkopts, commitopts, commitopts2, logopts |
25 from mercurial.commands import walkopts, commitopts, commitopts2, logopts |
26 from mercurial import hg |
26 from mercurial import hg |
27 |
27 |
28 ### util function |
28 ### util function |
29 ############################# |
29 ############################# |
|
30 |
30 def noderange(repo, revsets): |
31 def noderange(repo, revsets): |
31 """The same as revrange but return node""" |
32 """The same as revrange but return node""" |
32 return map(repo.changelog.node, |
33 return map(repo.changelog.node, |
33 scmutil.revrange(repo, revsets)) |
34 scmutil.revrange(repo, revsets)) |
34 |
35 |
35 |
36 def warnobserrors(orig, ui, repo, *args, **kwargs): |
36 |
|
37 def warnunstable(orig, ui, repo, *args, **kwargs): |
|
38 """display warning is the command resulted in more instable changeset""" |
37 """display warning is the command resulted in more instable changeset""" |
39 priorunstables = len(repo.revs('unstable()')) |
38 priorunstables = len(repo.revs('unstable()')) |
|
39 priorlatecomers = len(repo.revs('latecomer()')) |
40 #print orig, priorunstables |
40 #print orig, priorunstables |
41 #print len(repo.revs('secret() - obsolete()')) |
41 #print len(repo.revs('secret() - obsolete()')) |
42 try: |
42 try: |
43 return orig(ui, repo, *args, **kwargs) |
43 return orig(ui, repo, *args, **kwargs) |
44 finally: |
44 finally: |
45 newunstables = len(repo.revs('unstable()')) - priorunstables |
45 newunstables = len(repo.revs('unstable()')) - priorunstables |
|
46 newlatecomers = len(repo.revs('latecomer()')) - priorlatecomers |
46 #print orig, newunstables |
47 #print orig, newunstables |
47 #print len(repo.revs('secret() - obsolete()')) |
48 #print len(repo.revs('secret() - obsolete()')) |
48 if newunstables > 0: |
49 if newunstables > 0: |
49 ui.warn(_('%i new unstables changesets\n') % newunstables) |
50 ui.warn(_('%i new unstables changesets\n') % newunstables) |
50 |
51 if newlatecomers > 0: |
51 |
52 ui.warn(_('%i new latecomers changesets\n') % newlatecomers) |
52 ### extension check |
|
53 ############################# |
|
54 |
|
55 |
53 |
56 ### changeset rewriting logic |
54 ### changeset rewriting logic |
57 ############################# |
55 ############################# |
58 |
56 |
59 def rewrite(repo, old, updates, head, newbases, commitopts): |
57 def rewrite(repo, old, updates, head, newbases, commitopts): |
134 revcount = len(repo) |
132 revcount = len(repo) |
135 newid = repo.commitctx(new) |
133 newid = repo.commitctx(new) |
136 new = repo[newid] |
134 new = repo[newid] |
137 created = len(repo) != revcount |
135 created = len(repo) != revcount |
138 if created: |
136 if created: |
139 # update the bookmark |
137 updatebookmarks(newid) |
140 if bm: |
|
141 repo._bookmarks[bm] = newid |
|
142 bookmarks.write(repo) |
|
143 |
|
144 # add evolution metadata |
138 # add evolution metadata |
145 repo.addobsolete(new.node(), old.node()) |
139 collapsed = set([u.node() for u in updates] + [old.node()]) |
146 for u in updates: |
140 repo.addcollapsedobsolete(collapsed, new.node()) |
147 repo.addobsolete(u.node(), old.node()) |
|
148 repo.addobsolete(new.node(), u.node()) |
|
149 oldbookmarks = repo.nodebookmarks(old.node()) |
|
150 for book in oldbookmarks: |
|
151 repo._bookmarks[book] = new.node() |
|
152 if oldbookmarks: |
|
153 bookmarks.write(repo) |
|
154 else: |
141 else: |
155 # newid is an existing revision. It could make sense to |
142 # newid is an existing revision. It could make sense to |
156 # replace revisions with existing ones but probably not by |
143 # replace revisions with existing ones but probably not by |
157 # default. |
144 # default. |
158 pass |
145 pass |
177 rebase.rebasenode(repo, orig.node(), dest.node(), |
164 rebase.rebasenode(repo, orig.node(), dest.node(), |
178 {node.nullrev: node.nullrev}, False) |
165 {node.nullrev: node.nullrev}, False) |
179 else: |
166 else: |
180 rebase.rebasenode(repo, orig.node(), dest.node(), |
167 rebase.rebasenode(repo, orig.node(), dest.node(), |
181 {node.nullrev: node.nullrev}) |
168 {node.nullrev: node.nullrev}) |
182 nodenew = rebase.concludenode(repo, orig.node(), dest.node(), node.nullid) |
169 try: |
|
170 nodenew = rebase.concludenode(repo, orig.node(), dest.node(), |
|
171 node.nullid) |
|
172 except util.Abort: |
|
173 repo.ui.write_err(_('/!\\ stabilize failed /!\\\n')) |
|
174 repo.ui.write_err(_('/!\\ Their is no "hg stabilize --continue" /!\\\n')) |
|
175 repo.ui.write_err(_('/!\\ use "hg up -C . ; hg stabilize --dry-run" /!\\\n')) |
|
176 raise |
183 oldbookmarks = repo.nodebookmarks(nodesrc) |
177 oldbookmarks = repo.nodebookmarks(nodesrc) |
184 if nodenew is not None: |
178 if nodenew is not None: |
185 phases.retractboundary(repo, destphase, [nodenew]) |
179 phases.retractboundary(repo, destphase, [nodenew]) |
186 repo.addobsolete(nodenew, nodesrc) |
180 repo.addobsolete(nodenew, nodesrc) |
187 for book in oldbookmarks: |
181 for book in oldbookmarks: |
218 ctx.rev())) |
211 ctx.rev())) |
219 if unstables: |
212 if unstables: |
220 return unstables[0] |
213 return unstables[0] |
221 return None |
214 return None |
222 |
215 |
|
216 def _bookmarksupdater(repo, oldid): |
|
217 """Return a callable update(newid) updating the current bookmark |
|
218 and bookmarks bound to oldid to newid. |
|
219 """ |
|
220 bm = bookmarks.readcurrent(repo) |
|
221 def updatebookmarks(newid): |
|
222 dirty = False |
|
223 if bm: |
|
224 repo._bookmarks[bm] = newid |
|
225 dirty = True |
|
226 oldbookmarks = repo.nodebookmarks(oldid) |
|
227 if oldbookmarks: |
|
228 for b in oldbookmarks: |
|
229 repo._bookmarks[b] = newid |
|
230 dirty = True |
|
231 if dirty: |
|
232 bookmarks.write(repo) |
|
233 return updatebookmarks |
|
234 |
223 ### new command |
235 ### new command |
224 ############################# |
236 ############################# |
225 cmdtable = {} |
237 cmdtable = {} |
226 command = cmdutil.command(cmdtable) |
238 command = cmdutil.command(cmdtable) |
227 |
239 |
228 @command('^stabilize|evolve', |
240 @command('^stabilize|evolve', |
229 [ |
241 [('n', 'dry-run', False, 'do not perform actions, print what to be done'), |
230 ('n', 'dry-run', False, 'Do nothing but printing what should be done'), |
242 ('A', 'any', False, 'stabilize any unstable changeset'),], |
231 ('A', 'any', False, 'Stabilize unstable change on any topological branch'), |
243 _('[OPTIONS]...')) |
232 ], |
|
233 '') |
|
234 def stabilize(ui, repo, **opts): |
244 def stabilize(ui, repo, **opts): |
235 """rebase an unstable changeset to make it stable again |
245 """rebase an unstable changeset to make it stable again |
236 |
246 |
237 By default, take the first unstable changeset which could be |
247 By default, take the first unstable changeset which could be |
238 rebased as child of the working directory parent revision or one |
248 rebased as child of the working directory parent revision or one |
344 for c in children: |
354 for c in children: |
345 displayer.show(c) |
355 displayer.show(c) |
346 ui.warn(_('Multiple non-obsolete children, explicitly update to one\n')) |
356 ui.warn(_('Multiple non-obsolete children, explicitly update to one\n')) |
347 return 1 |
357 return 1 |
348 |
358 |
349 |
359 @command('^kill|obsolete|prune', |
350 @command('^kill|obsolete', |
360 [('n', 'new', [], _("successor changeset"))], |
351 [ |
361 _('[OPTION] REV...')) |
352 ('n', 'new', [], _("New changeset that justify this one to be killed")) |
|
353 ], |
|
354 '<revs>') |
|
355 def kill(ui, repo, *revs, **opts): |
362 def kill(ui, repo, *revs, **opts): |
356 """mark a changeset as obsolete |
363 """mark a changeset as obsolete |
357 |
364 |
358 This update the parent directory to a not-killed parent if the current |
365 This update the parent directory to a not-killed parent if the current |
359 working directory parent are killed. |
366 working directory parent are killed. |
387 wlock.release() |
394 wlock.release() |
388 |
395 |
389 @command('^amend|refresh', |
396 @command('^amend|refresh', |
390 [('A', 'addremove', None, |
397 [('A', 'addremove', None, |
391 _('mark new/missing files as added/removed before committing')), |
398 _('mark new/missing files as added/removed before committing')), |
392 ('n', 'note', '', |
399 ('n', 'note', '', _('use text as commit message for this update')), |
393 _('use text as commit message for this update')), |
400 ('c', 'change', '', _('specifies the changesets to amend'), _('REV')), |
394 ('c', 'change', '', |
401 ('e', 'edit', False, _('invoke editor on commit messages')), |
395 _('specifies the changeset to amend'), _('REV')), |
|
396 ('e', 'edit', False, |
|
397 _('edit commit message.'), _('')), |
|
398 ] + walkopts + commitopts + commitopts2, |
402 ] + walkopts + commitopts + commitopts2, |
399 _('[OPTION]... [FILE]...')) |
403 _('[OPTION]... [FILE]...')) |
400 |
|
401 def amend(ui, repo, *pats, **opts): |
404 def amend(ui, repo, *pats, **opts): |
402 """combine a changeset with updates and replace it with a new one |
405 """combine a changeset with updates and replace it with a new one |
403 |
406 |
404 Commits a new changeset incorporating both the changes to the given files |
407 Commits a new changeset incorporating both the changes to the given files |
405 and all the changes from the current parent changeset into the repository. |
408 and all the changes from the current parent changeset into the repository. |
406 |
409 |
407 See :hg:`commit` for details about committing changes. |
410 See :hg:`commit` for details about committing changes. |
408 |
411 |
409 If you don't specify -m, the parent's message will be reused. |
412 If you don't specify -m, the parent's message will be reused. |
410 |
413 |
411 If you specify --change, amend additionally considers all changesets between |
414 If you specify --change, amend additionally considers all |
412 the indicated changeset and the working copy parent as updates to be subsumed. |
415 changesets between the indicated changeset and the working copy |
413 This allows you to commit updates manually first. As a special shorthand you |
416 parent as updates to be subsumed. |
414 can say `--amend .` instead of '--amend p1(p1())', which subsumes your latest |
|
415 commit as an update of its parent. |
|
416 |
417 |
417 Behind the scenes, Mercurial first commits the update as a regular child |
418 Behind the scenes, Mercurial first commits the update as a regular child |
418 of the current parent. Then it creates a new commit on the parent's parents |
419 of the current parent. Then it creates a new commit on the parent's parents |
419 with the updated contents. Then it changes the working copy parent to this |
420 with the updated contents. Then it changes the working copy parent to this |
420 new combined changeset. Finally, the old changeset and its update are hidden |
421 new combined changeset. Finally, the old changeset and its update are hidden |
422 |
423 |
423 Returns 0 on success, 1 if nothing changed. |
424 Returns 0 on success, 1 if nothing changed. |
424 """ |
425 """ |
425 |
426 |
426 # determine updates to subsume |
427 # determine updates to subsume |
427 change = opts.get('change', '.') |
428 old = scmutil.revsingle(repo, opts.get('change') or '.') |
428 if change == '.': |
|
429 change = 'p1(p1())' |
|
430 old = scmutil.revsingle(repo, change) |
|
431 |
429 |
432 lock = repo.lock() |
430 lock = repo.lock() |
433 try: |
431 try: |
434 wlock = repo.wlock() |
432 wlock = repo.wlock() |
435 try: |
433 try: |
436 if not old.phase(): |
434 if old.phase() == phases.public: |
437 raise util.Abort(_("can not rewrite immutable changeset %s") % old) |
435 raise util.Abort(_("can not rewrite immutable changeset %s") |
|
436 % old) |
438 oldphase = old.phase() |
437 oldphase = old.phase() |
439 # commit current changes as update |
438 # commit current changes as update |
440 # code copied from commands.commit to avoid noisy messages |
439 # code copied from commands.commit to avoid noisy messages |
441 ciopts = dict(opts) |
440 ciopts = dict(opts) |
442 ciopts.pop('message', None) |
441 ciopts.pop('message', None) |
443 ciopts.pop('logfile', None) |
442 ciopts.pop('logfile', None) |
444 ciopts['message'] = opts.get('note') or ('amends %s' % old.hex()) |
443 ciopts['message'] = opts.get('note') or ('amends %s' % old.hex()) |
445 e = cmdutil.commiteditor |
444 e = cmdutil.commiteditor |
446 def commitfunc(ui, repo, message, match, opts): |
445 def commitfunc(ui, repo, message, match, opts): |
447 return repo.commit(message, opts.get('user'), opts.get('date'), match, |
446 return repo.commit(message, opts.get('user'), opts.get('date'), |
448 editor=e) |
447 match, editor=e) |
449 revcount = len(repo) |
448 revcount = len(repo) |
450 tempid = cmdutil.commit(ui, repo, commitfunc, pats, ciopts) |
449 tempid = cmdutil.commit(ui, repo, commitfunc, pats, ciopts) |
451 if len(repo) == revcount: |
450 if len(repo) == revcount: |
452 # No revision created |
451 # No revision created |
453 tempid = None |
452 tempid = None |
488 finally: |
485 finally: |
489 wlock.release() |
486 wlock.release() |
490 finally: |
487 finally: |
491 lock.release() |
488 lock.release() |
492 |
489 |
493 |
490 def _commitfiltered(repo, ctx, match): |
494 |
491 """Recommit ctx with changed files not in match. Return the new |
|
492 node identifier, or None if nothing changed. |
|
493 """ |
|
494 base = ctx.p1() |
|
495 m, a, r = repo.status(base, ctx)[:3] |
|
496 allfiles = set(m + a + r) |
|
497 files = set(f for f in allfiles if not match(f)) |
|
498 if files == allfiles: |
|
499 return None |
|
500 |
|
501 # Filter copies |
|
502 copied = copies.pathcopies(base, ctx) |
|
503 copied = dict((src, dst) for src, dst in copied.iteritems() |
|
504 if dst in files) |
|
505 def filectxfn(repo, memctx, path): |
|
506 if path not in ctx: |
|
507 raise IOError() |
|
508 fctx = ctx[path] |
|
509 flags = fctx.flags() |
|
510 mctx = context.memfilectx(fctx.path(), fctx.data(), |
|
511 islink='l' in flags, |
|
512 isexec='x' in flags, |
|
513 copied=copied.get(path)) |
|
514 return mctx |
|
515 |
|
516 new = context.memctx(repo, |
|
517 parents=[base.node(), node.nullid], |
|
518 text=ctx.description(), |
|
519 files=files, |
|
520 filectxfn=filectxfn, |
|
521 user=ctx.user(), |
|
522 date=ctx.date(), |
|
523 extra=ctx.extra()) |
|
524 # commitctx always create a new revision, no need to check |
|
525 newid = repo.commitctx(new) |
|
526 return newid |
|
527 |
|
528 def _uncommitdirstate(repo, oldctx, match): |
|
529 """Fix the dirstate after switching the working directory from |
|
530 oldctx to a copy of oldctx not containing changed files matched by |
|
531 match. |
|
532 """ |
|
533 ctx = repo['.'] |
|
534 ds = repo.dirstate |
|
535 copies = dict(ds.copies()) |
|
536 m, a, r = repo.status(oldctx.p1(), oldctx, match=match)[:3] |
|
537 for f in m: |
|
538 if ds[f] == 'r': |
|
539 # modified + removed -> removed |
|
540 continue |
|
541 ds.normallookup(f) |
|
542 |
|
543 for f in a: |
|
544 if ds[f] == 'r': |
|
545 # added + removed -> unknown |
|
546 ds.drop(f) |
|
547 elif ds[f] != 'a': |
|
548 ds.add(f) |
|
549 |
|
550 for f in r: |
|
551 if ds[f] == 'a': |
|
552 # removed + added -> normal |
|
553 ds.normallookup(f) |
|
554 elif ds[f] != 'r': |
|
555 ds.remove(f) |
|
556 |
|
557 # Merge old parent and old working dir copies |
|
558 oldcopies = {} |
|
559 for f in (m + a): |
|
560 src = oldctx[f].renamed() |
|
561 if src: |
|
562 oldcopies[f] = src[0] |
|
563 oldcopies.update(copies) |
|
564 copies = dict((dst, oldcopies.get(src, src)) |
|
565 for dst, src in oldcopies.iteritems()) |
|
566 # Adjust the dirstate copies |
|
567 for dst, src in copies.iteritems(): |
|
568 if (src not in ctx or dst in ctx or ds[dst] != 'a'): |
|
569 src = None |
|
570 ds.copy(src, dst) |
|
571 |
|
572 @command('^uncommit', |
|
573 [('a', 'all', None, _('uncommit all changes when no arguments given')), |
|
574 ] + commands.walkopts, |
|
575 _('[OPTION]... [NAME]')) |
|
576 def uncommit(ui, repo, *pats, **opts): |
|
577 """move changes from parent revision to working directory |
|
578 |
|
579 Changes to selected files in parent revision appear again as |
|
580 uncommitted changed in the working directory. A new revision |
|
581 without selected changes is created, becomes the new parent and |
|
582 obsoletes the previous one. |
|
583 |
|
584 The --include option specify pattern to uncommit |
|
585 The --exclude option specify pattern to keep in the commit |
|
586 |
|
587 Return 0 if changed files are uncommitted. |
|
588 """ |
|
589 lock = repo.lock() |
|
590 try: |
|
591 wlock = repo.wlock() |
|
592 try: |
|
593 wctx = repo[None] |
|
594 if len(wctx.parents()) <= 0: |
|
595 raise util.Abort(_("cannot uncommit null changeset")) |
|
596 if len(wctx.parents()) > 1: |
|
597 raise util.Abort(_("cannot uncommit while merging")) |
|
598 old = repo['.'] |
|
599 if old.phase() == phases.public: |
|
600 raise util.Abort(_("cannot rewrite immutable changeset")) |
|
601 if len(old.parents()) > 1: |
|
602 raise util.Abort(_("cannot uncommit merge changeset")) |
|
603 oldphase = old.phase() |
|
604 updatebookmarks = _bookmarksupdater(repo, old.node()) |
|
605 # Recommit the filtered changeset |
|
606 newid = None |
|
607 if (pats or opts.get('include') or opts.get('exclude') |
|
608 or opts.get('all')): |
|
609 match = scmutil.match(old, pats, opts) |
|
610 newid = _commitfiltered(repo, old, match) |
|
611 if newid is None: |
|
612 raise util.Abort(_('nothing to uncommit')) |
|
613 # Move local changes on filtered changeset |
|
614 repo.addobsolete(newid, old.node()) |
|
615 phases.retractboundary(repo, oldphase, [newid]) |
|
616 repo.dirstate.setparents(newid, node.nullid) |
|
617 _uncommitdirstate(repo, old, match) |
|
618 updatebookmarks(newid) |
|
619 if not repo[newid].files(): |
|
620 ui.warn(_("new changeset is empty\n")) |
|
621 ui.status(_('(use "hg kill ." to remove it)\n')) |
|
622 finally: |
|
623 wlock.release() |
|
624 finally: |
|
625 lock.release() |
495 |
626 |
496 def commitwrapper(orig, ui, repo, *arg, **kwargs): |
627 def commitwrapper(orig, ui, repo, *arg, **kwargs): |
497 lock = repo.lock() |
628 lock = repo.lock() |
498 try: |
629 try: |
499 obsoleted = kwargs.get('obsolete', []) |
630 obsoleted = kwargs.get('obsolete', []) |
545 except KeyError: |
676 except KeyError: |
546 rebase = None |
677 rebase = None |
547 raise error.Abort(_('evolution extension require rebase extension.')) |
678 raise error.Abort(_('evolution extension require rebase extension.')) |
548 |
679 |
549 entry = extensions.wrapcommand(commands.table, 'commit', commitwrapper) |
680 entry = extensions.wrapcommand(commands.table, 'commit', commitwrapper) |
550 entry[1].append(('o', 'obsolete', [], _("this commit obsolet this revision"))) |
681 entry[1].append(('o', 'obsolete', [], |
|
682 _("make commit obsolete this revision"))) |
551 entry = extensions.wrapcommand(commands.table, 'graft', graftwrapper) |
683 entry = extensions.wrapcommand(commands.table, 'graft', graftwrapper) |
552 entry[1].append(('o', 'obsolete', [], _("this graft obsolet this revision"))) |
684 entry[1].append(('o', 'obsolete', [], |
553 entry[1].append(('O', 'old-obsolete', False, _("graft result obsolete graft source"))) |
685 _("make graft obsoletes this revision"))) |
|
686 entry[1].append(('O', 'old-obsolete', False, |
|
687 _("make graft obsoletes its source"))) |
554 |
688 |
555 # warning about more obsolete |
689 # warning about more obsolete |
556 for cmd in ['commit', 'push', 'pull', 'graft']: |
690 for cmd in ['commit', 'push', 'pull', 'graft', 'phase', 'unbundle']: |
557 entry = extensions.wrapcommand(commands.table, cmd, warnunstable) |
691 entry = extensions.wrapcommand(commands.table, cmd, warnobserrors) |
558 for cmd in ['kill', 'amend']: |
692 for cmd in ['amend', 'kill', 'uncommit']: |
559 entry = extensions.wrapcommand(cmdtable, cmd, warnunstable) |
693 entry = extensions.wrapcommand(cmdtable, cmd, warnobserrors) |
560 |
694 |
561 if rebase is not None: |
695 if rebase is not None: |
562 entry = extensions.wrapcommand(rebase.cmdtable, 'rebase', warnunstable) |
696 entry = extensions.wrapcommand(rebase.cmdtable, 'rebase', warnobserrors) |