def prune(ui, repo, *revs, **opts): """hide changesets by marking them obsolete Pruned changesets are obsolete with no successors. If they also have no descendants, they are hidden (invisible to all commands). Non-obsolete descendants of pruned changesets become "unstable". Use :hg:`evolve` to handle this situation. When you prune the parent of your working copy, Mercurial updates the working copy to a non-obsolete parent. You can use ``--succ`` to tell Mercurial that a newer version (successor) of the pruned changeset exists. Mercurial records successor revisions in obsolescence markers. You can use the ``--biject`` option to specify a 1-1 mapping (bijection) between revisions to pruned (precursor) and successor changesets. This option may be removed in a future release (with the functionality provided automatically). If you specify multiple revisions in ``--succ``, you are recording a "split" and must acknowledge it by passing ``--split``. Similarly, when you prune multiple changesets with a single successor, you must pass the ``--fold`` option. """ if opts.get("keep", False): hint = "strip-uncommit" else: hint = "strip-hide" hintutil.trigger(hint) revs = scmutil.revrange(repo, list(revs) + opts.get("rev", [])) succs = opts.get("succ", []) bookmarks = set(opts.get("bookmark", ())) metadata = _getmetadata(**opts) biject = opts.get("biject") fold = opts.get("fold") split = opts.get("split") options = [o for o in ("biject", "fold", "split") if opts.get(o)] if 1 < len(options): raise error.Abort(_("can only specify one of %s") % ", ".join(options)) if bookmarks: revs += bookmarksmod.reachablerevs(repo, bookmarks) if not revs: # No revs are reachable exclusively from these bookmarks, just # delete the bookmarks. with repo.wlock(), repo.lock(), repo.transaction( "prune-bookmarks") as tr: bookmarksmod.delete(repo, tr, bookmarks) for bookmark in sorted(bookmarks): ui.write(_("bookmark '%s' deleted\n") % bookmark) return 0 if not revs: raise error.Abort(_("nothing to prune")) wlock = lock = tr = None try: wlock = repo.wlock() lock = repo.lock() tr = repo.transaction("prune") # defines pruned changesets precs = [] revs.sort() for p in revs: cp = repo[p] if not cp.mutable(): # note: createmarkers() would have raised something anyway raise error.Abort( "cannot prune immutable changeset: %s" % cp, hint="see 'hg help phases' for details", ) precs.append(cp) if not precs: raise error.Abort("nothing to prune") # defines successors changesets sucs = scmutil.revrange(repo, succs) sucs.sort() sucs = tuple(repo[n] for n in sucs) if not biject and len(sucs) > 1 and len(precs) > 1: msg = "Can't use multiple successors for multiple precursors" hint = _("use --biject to mark a series as a replacement" " for another") raise error.Abort(msg, hint=hint) elif biject and len(sucs) != len(precs): msg = "Can't use %d successors for %d precursors" % (len(sucs), len(precs)) raise error.Abort(msg) elif (len(precs) == 1 and len(sucs) > 1) and not split: msg = "please add --split if you want to do a split" raise error.Abort(msg) elif len(sucs) == 1 and len(precs) > 1 and not fold: msg = "please add --fold if you want to do a fold" raise error.Abort(msg) elif biject: relations = [(p, (s, )) for p, s in zip(precs, sucs)] else: relations = [(p, sucs) for p in precs] wdp = repo["."] if len(sucs) == 1 and len(precs) == 1 and wdp in precs: # '.' killed, so update to the successor newnode = sucs[0] else: # update to an unkilled parent newnode = wdp while newnode in precs or newnode.obsolete(): newnode = newnode.parents()[0] if newnode.node() != wdp.node(): if opts.get("keep", False): # This is largely the same as the implementation in # strip.stripcmd(). We might want to refactor this somewhere # common at some point. # only reset the dirstate for files that would actually change # between the working context and uctx descendantrevs = repo.revs("%d::." % newnode.rev()) changedfiles = [] for rev in descendantrevs: # blindly reset the files, regardless of what actually # changed changedfiles.extend(repo[rev].files()) # reset files that only changed in the dirstate too dirstate = repo.dirstate dirchanges = [f for f in dirstate if dirstate[f] != "n"] changedfiles.extend(dirchanges) repo.dirstate.rebuild(newnode.node(), newnode.manifest(), changedfiles) dirstate.write(tr) else: bookactive = repo._activebookmark # Active bookmark that we don't want to delete (with -B option) # we deactivate and move it before the update and reactivate it # after movebookmark = bookactive and not bookmarks if movebookmark: bookmarksmod.deactivate(repo) changes = [(bookactive, newnode.node())] repo._bookmarks.applychanges(repo, tr, changes) commands.update(ui, repo, newnode.hex()) ui.status( _("working directory now at %s\n") % ui.label(str(newnode), "evolve.node")) if movebookmark: bookmarksmod.activate(repo, bookactive) # update bookmarks if bookmarks: with repo.wlock(), repo.lock(), repo.transaction( "prune-bookmarks") as tr: bookmarksmod.delete(repo, tr, bookmarks) for bookmark in sorted(bookmarks): ui.write(_("bookmark '%s' deleted\n") % bookmark) # create markers obsolete.createmarkers(repo, relations, metadata=metadata, operation="prune") # hide nodes visibility.remove(repo, [c.node() for c in precs]) # informs that changeset have been pruned ui.status(_("%i changesets pruned\n") % len(precs)) for ctx in repo.set("bookmark() and %ld", precs): # used to be: # # ldest = list(repo.set('max((::%d) - obsolete())', ctx)) # if ldest: # c = ldest[0] # # but then revset took a lazy arrow in the knee and became much # slower. The new forms makes as much sense and a much faster. for dest in ctx.ancestors(): if not dest.obsolete(): updatebookmarks = common.bookmarksupdater(repo, ctx.node()) updatebookmarks(dest.node()) break tr.close() finally: lockmod.release(tr, lock, wlock)
def split(ui, repo, *revs, **opts): """split a changeset into smaller changesets Prompt for hunks to be selected until exhausted. Each selection of hunks will form a separate changeset, in order from parent to child: the first selection will form the first changeset, the second selection will form the second changeset, and so on. Operates on the current revision by default. Use --rev to split a given changeset instead. """ newcommits = [] revarg = (list(revs) + opts.get("rev")) or ["."] if len(revarg) != 1: msg = _("more than one revset is given") hnt = _( "use either `hg split <rs>` or `hg split --rev <rs>`, not both") raise error.Abort(msg, hint=hnt) rev = scmutil.revsingle(repo, revarg[0]) if opts.get("no_rebase"): torebase = () else: torebase = list( map(hex, repo.nodes("descendants(%d) - (%d)", rev, rev))) with repo.wlock(), repo.lock(): cmdutil.bailifchanged(repo) if torebase: cmdutil.checkunfinished(repo) ctx = repo[rev] r = ctx.hex() allowunstable = visibility.tracking(repo) or obsolete.isenabled( repo, obsolete.allowunstableopt) if not allowunstable: # XXX We should check head revs if repo.revs("(%d::) - %d", rev, rev): raise error.Abort( _("cannot split commit: %s not a head") % ctx) if len(ctx.parents()) > 1: raise error.Abort(_("cannot split merge commits")) prev = ctx.p1() bmupdate = common.bookmarksupdater(repo, ctx.node()) bookactive = repo._activebookmark if bookactive is not None: repo.ui.status(_("(leaving bookmark %s)\n") % repo._activebookmark) bookmarks.deactivate(repo) hg.update(repo, prev) commands.revert(ui, repo, rev=r, all=True) def haschanges(): modified, added, removed, deleted = repo.status()[:4] return modified or added or removed or deleted # We need to detect the case where the user selects all remaining # changes, as that will end the split. That's the commit we want to # mark as the result of the split. To do this, wrap the recordfilter # function and compare the output to see if it contains all the # originalchunks. shouldrecordmutation = [False] def mutinfo(extra): if shouldrecordmutation[0]: return mutation.record( repo, extra, [ctx.node()], "split", splitting=[c.node() for c in newcommits], ) def recordfilter(ui, originalchunks, operation=None): chunks, newopts = cmdutil.recordfilter(ui, originalchunks, operation) if cmdutil.comparechunks(chunks, originalchunks): shouldrecordmutation[0] = True return chunks, newopts msg = ("HG: This is the original pre-split commit message. " "Edit it as appropriate.\n\n") msg += ctx.description() opts["message"] = msg opts["edit"] = True opts["_commitmutinfofunc"] = mutinfo try: while haschanges(): pats = () with repo.transaction("split"): cmdutil.dorecord(ui, repo, commands.commit, "commit", False, recordfilter, *pats, **opts) # TODO: Does no seem like the best way to do this # We should make dorecord return the newly created commit newcommits.append(repo["."]) if haschanges(): if ui.prompt("Done splitting? [yN]", default="n") == "y": shouldrecordmutation[0] = True with repo.transaction("split"): commands.commit(ui, repo, **opts) newcommits.append(repo["."]) break else: ui.status(_("no more change to split\n")) except Exception: # Rollback everything hg.updaterepo(repo, r, True) # overwrite=True if newcommits: visibility.remove(repo, [c.node() for c in newcommits]) if bookactive is not None: bookmarks.activate(repo, bookactive) raise if newcommits: phabdiffs = {} for c in newcommits: phabdiff = diffprops.parserevfromcommitmsg( repo[c].description()) if phabdiff: phabdiffs.setdefault(phabdiff, []).append(c) if any(len(commits) > 1 for commits in phabdiffs.values()): hintutil.trigger("split-phabricator", ui.config("split", "phabricatoradvice")) tip = repo[newcommits[-1]] with repo.transaction("post-split"): bmupdate(tip.node()) if bookactive is not None: bookmarks.activate(repo, bookactive) if obsolete.isenabled(repo, obsolete.createmarkersopt): obsolete.createmarkers(repo, [(repo[r], newcommits)], operation="split") if torebase: rebaseopts = {"dest": "_destrestack(SRC)", "rev": torebase} rebase.rebase(ui, repo, **rebaseopts) unfi = repo with repo.transaction("post-split-hide"): visibility.remove(repo, [unfi[r].node()])
def _backupactivebookmark(repo): activebookmark = repo._activebookmark if activebookmark: bookmarks.deactivate(repo) return activebookmark