def drop(ui, repo, *revs, **opts): """drop changeset from stack """ if not rebasemod: raise error.Abort(_("required extensions not detected")) cmdutil.checkunfinished(repo) cmdutil.bailifchanged(repo) revs = scmutil.revrange(repo, list(revs) + opts.get("rev")) if not revs: raise error.Abort(_("no revision to drop was provided")) # currently drop supports dropping only one changeset at a time if len(revs) > 1: raise error.Abort(_("only one revision can be dropped at a time")) revid = revs.first() changectx = repo[revid] if changectx.phase() == phases.public: raise error.Abort(_("public changeset which landed cannot be dropped")) parents = repo.revs("parents(%s)", revid) if len(parents) > 1: raise error.Abort(_("merge changeset cannot be dropped")) elif len(parents) == 0: raise error.Abort(_("root changeset cannot be dropped")) _showrev(ui, repo, revid) descendants = repo.revs("(%d::) - %d", revid, revid) parent = parents.first() with repo.wlock(): with repo.lock(): with repo.transaction("drop"): if len(descendants) > 0: try: rebasemod.rebase(ui, repo, dest=str(parent), rev=descendants) except error.InterventionRequired: ui.warn( _("conflict occurred during drop: " + "please fix it by running " + "'hg rebase --continue', " + "and then re-run 'hg drop'\n")) raise scmutil.cleanupnodes(repo, [changectx.node()], "drop")
def _deleteunreachable(repo, ctx): """Deletes all ancestor and descendant commits of the given revision that aren't reachable from another bookmark. """ keepheads = "bookmark() + ." try: extensions.find("remotenames") keepheads += " + remotenames()" except KeyError: pass hidenodes = list(repo.nodes("(draft() & ::%s) - ::(%r)", ctx.rev(), keepheads)) if hidenodes: with repo.lock(): scmutil.cleanupnodes(repo, hidenodes, "reset") repo.ui.status( _n("%d changeset hidden\n", "%d changesets hidden\n", len(hidenodes)) % len(hidenodes) )
def commit(self): """commit changes. update self.finalnode, self.replacemap""" with self.repo.wlock(), self.repo.lock(): with self.repo.transaction("absorb"): self._commitstack() replacements = { old: [new] if new else [] for old, new in self.replacemap.items() } moves = scmutil.cleanupnodes(self.repo, replacements, "absorb") newwd = moves.get(self.repo["."].node()) if newwd is not None: self._moveworkingdirectoryparent(newwd) return self.finalnode
def fold(ui, repo, *revs, **opts): """combine multiple commits into a single commit With --from, folds all the revisions linearly between the current revision and the specified revision. With --exact, folds only the specified revisions while ignoring the revision currently checked out. The given revisions must form a linear unbroken chain. .. container:: verbose Some examples: - Fold from the current revision to its parent:: hg fold --from .^ - Fold all draft revisions into the current revision:: hg fold --from 'draft()' See :hg:`help phases` for more about draft revisions and :hg:`help revsets` for more about the `draft()` keyword - Fold revisions between 3 and 6 into the current revision:: hg fold --from 3::6 - Fold revisions 3 and 4: hg fold "3 + 4" --exact - Only fold revisions linearly between foo and @:: hg fold foo::@ --exact """ revs = list(revs) revs.extend(opts["rev"]) if not revs: raise error.Abort(_("no revisions specified")) revs = scmutil.revrange(repo, revs) if opts.get("no_rebase"): torebase = () else: torebase = repo.revs("descendants(%ld) - (%ld)", revs, revs) if opts["from"] and opts["exact"]: raise error.Abort(_("cannot use both --from and --exact")) elif opts["from"]: # Try to extend given revision starting from the working directory extrevs = repo.revs("(%ld::.) or (.::%ld)", revs, revs) discardedrevs = [r for r in revs if r not in extrevs] if discardedrevs: msg = _("cannot fold non-linear revisions") hint = _("given revisions are unrelated to parent of working" " directory") raise error.Abort(msg, hint=hint) revs = extrevs elif opts["exact"]: # Nothing to do; "revs" is already set correctly pass else: raise error.Abort(_("must specify either --from or --exact")) if not revs: raise error.Abort( _("specified revisions evaluate to an empty set"), hint=_("use different revision arguments"), ) elif len(revs) == 1: ui.write_err(_("single revision specified, nothing to fold\n")) return 1 with repo.wlock(), repo.lock(), ui.formatter("fold", opts) as fm: fm.startitem() root, head = _foldcheck(repo, revs) with repo.transaction("fold") as tr: commitopts = opts.copy() allctx = [repo[r] for r in revs] targetphase = max(c.phase() for c in allctx) if (commitopts.get("message") or commitopts.get("logfile") or commitopts.get("reuse_message")): commitopts["edit"] = False else: msgs = ["HG: This is a fold of %d changesets." % len(allctx)] msgs += [ "HG: Commit message of %s.\n\n%s\n" % (node.short(c.node()), c.description()) for c in allctx ] commitopts["message"] = "\n".join(msgs) commitopts["edit"] = True newid, unusedvariable = common.rewrite( repo, root, allctx, head, [root.p1().node(), root.p2().node()], commitopts=commitopts, mutop="fold", ) phases.retractboundary(repo, tr, targetphase, [newid]) replacements = {ctx.node(): (newid, ) for ctx in allctx} nodechanges = { fm.hexfunc(ctx.node()): [fm.hexfunc(newid)] for ctx in allctx } fm.data(nodechanges=fm.formatdict(nodechanges)) scmutil.cleanupnodes(repo, replacements, "fold") fm.condwrite(not ui.quiet, "count", "%i changesets folded\n", len(revs)) if repo["."].rev() in revs: hg.update(repo, newid) if torebase: common.restackonce(ui, repo, repo[newid].rev())
def metaedit(ui, repo, templ, *revs, **opts): """edit commit message and other metadata Edit commit message for the current commit. By default, opens your default editor so that you can edit the commit message interactively. Specify -m to specify the commit message on the command line. To edit the message for a different commit, specify -r. To edit the messages of multiple commits, specify --batch. You can edit other pieces of commit metadata, namely the user or date, by specifying -u or -d, respectively. The expected format for user is 'Full Name <*****@*****.**>'. There is also automation-friendly JSON input mode which allows the caller to provide the mapping between commit and new message and username in the following format: { "<commit_hash>": { "message": "<message>", "user": "******" // optional } } .. note:: You can specify --fold to fold multiple revisions into one when the given revisions form a linear unbroken chain. However, :hg:`fold` is the preferred command for this purpose. See :hg:`help fold` for more information. .. container:: verbose Some examples: - Edit the commit message for the current commit:: hg metaedit - Change the username for the current commit:: hg metaedit --user 'New User <*****@*****.**>' """ revs = list(revs) revs.extend(opts["rev"]) if not revs: if opts["fold"]: raise error.Abort(_("revisions must be specified with --fold")) revs = ["."] with repo.wlock(), repo.lock(): revs = scmutil.revrange(repo, revs) msgmap = { } # {node: message}, predefined messages, currently used by --batch usermap = { } # {node: author}, predefined authors, used by --jsoninputfile if opts["fold"]: root, head = fold._foldcheck(repo, revs) else: if repo.revs("%ld and public()", revs): raise error.Abort( _("cannot edit commit information for public " "revisions")) root = head = repo[revs.first()] wctx = repo[None] p1 = wctx.p1() tr = repo.transaction("metaedit") newp1 = None try: commitopts = opts.copy() allctx = [repo[r] for r in revs] jsoninputfile = None if any( commitopts.get(name) for name in ["message", "logfile", "reuse_message"]): commitopts["edit"] = False else: if opts["fold"]: msgs = [ _("HG: This is a fold of %d changesets.") % len(allctx) ] msgs += [ _("HG: Commit message of %s.\n\n%s\n") % (nodemod.short(c.node()), c.description()) for c in allctx ] else: if opts["batch"] and len(revs) > 1: msgmap = editmessages(repo, revs) msgs = [head.description()] jsoninputfile = opts.get("json_input_file") if jsoninputfile: try: if cmdutil.isstdiofilename(jsoninputfile): inputjson = pycompat.decodeutf8(ui.fin.read()) else: inputjson = pycompat.decodeutf8( util.readfile(jsoninputfile)) msgusermap = json.loads(inputjson) except IOError as inst: raise error.Abort( _("can't read JSON input file '%s': %s") % (jsoninputfile, encoding.strtolocal(inst.strerror))) except ValueError as inst: raise error.Abort( _("can't decode JSON input file '%s': %s") % (jsoninputfile, str(inst))) if not isinstance(msgusermap, dict): raise error.Abort( _("JSON input is not a dictionary (see --help for input format)" )) try: msgmap = { bin(node): msguser.get("message") for (node, msguser) in msgusermap.items() if "message" in msguser } usermap = { bin(node): msguser.get("user") for (node, msguser) in msgusermap.items() if "user" in msguser } except TypeError: raise error.Abort(_("invalid JSON input")) commitopts["message"] = "\n".join(msgs) commitopts["edit"] = True if root == head: # fast path: use metarewrite replacemap = {} # adding commitopts to the revisions to metaedit allctxopt = [{ "ctx": ctx, "commitopts": commitopts } for ctx in allctx] # all descendats that can be safely rewritten newunstable = common.newunstable(repo, revs) newunstableopt = [{ "ctx": ctx } for ctx in [repo[r] for r in newunstable]] # we need to edit descendants with the given revisions to not to # corrupt the stacks if _histediting(repo): ui.note( _("during histedit, the descendants of " "the edited commit weren't auto-rebased\n")) else: allctxopt += newunstableopt # we need topological order for all if mutation.enabled(repo): allctxopt = mutation.toposort( repo, allctxopt, nodefn=lambda copt: copt["ctx"].node()) else: allctxopt = sorted(allctxopt, key=lambda copt: copt["ctx"].rev()) def _rewritesingle(c, _commitopts): # Predefined message overrides other message editing choices. msg = msgmap.get(c.node()) if jsoninputfile: _commitopts["edit"] = False if msg is not None: _commitopts["message"] = msg _commitopts["edit"] = False user = usermap.get(c.node()) if user is not None: _commitopts["user"] = user if _commitopts.get("edit", False): msg = "HG: Commit message of changeset %s\n%s" % ( str(c), c.description(), ) _commitopts["message"] = msg bases = [ replacemap.get(c.p1().node(), c.p1().node()), replacemap.get(c.p2().node(), c.p2().node()), ] newid, created = common.metarewrite(repo, c, bases, commitopts=_commitopts) if created: replacemap[c.node()] = newid for copt in allctxopt: _rewritesingle( copt["ctx"], copt.get("commitopts", {"date": commitopts.get("date") or None}), ) if p1.node() in replacemap: repo.setparents(replacemap[p1.node()]) if len(replacemap) > 0: mapping = dict( map( lambda oldnew: (oldnew[0], [oldnew[1]]), pycompat.iteritems(replacemap), )) templ.setprop("nodereplacements", mapping) scmutil.cleanupnodes(repo, mapping, "metaedit") # TODO: set poroper phase boundaries (affects secret # phase only) else: ui.status(_("nothing changed\n")) return 1 else: # slow path: create a new commit targetphase = max(c.phase() for c in allctx) # TODO: if the author and message are the same, don't create a # new hash. Right now we create a new hash because the date can # be different. newid, created = common.rewrite( repo, root, allctx, head, [root.p1().node(), root.p2().node()], commitopts=commitopts, mutop="metaedit", ) if created: if p1.rev() in revs: newp1 = newid phases.retractboundary(repo, tr, targetphase, [newid]) mapping = dict([(repo[rev].node(), [newid]) for rev in revs]) templ.setprop("nodereplacements", mapping) scmutil.cleanupnodes(repo, mapping, "metaedit") else: ui.status(_("nothing changed\n")) return 1 tr.close() finally: tr.release() if opts["fold"]: ui.status(_("%i changesets folded\n") % len(revs)) if newp1 is not None: hg.update(repo, newp1)
def amendtocommit(ui, repo, commitspec, pats=None, opts=None): """amend to a specific commit This works by patching the working diff on to the specified commit and then performing a simplified rebase of the stack's tail on to the amended ancestor. commitspec must refer to a single commit that is a linear ancestor of ".". """ with repo.wlock(), repo.lock(), repo.transaction("amend"): revs = list(scmutil.revrange(repo, [commitspec])) if len(revs) != 1: raise error.Abort( _("'%s' must refer to a single changeset") % commitspec) draftctxs = list(repo.revs("(%d)::.", revs[0]).iterctx()) if len(draftctxs) == 0: raise error.Abort( _("revision '%s' is not an ancestor of the working copy") % commitspec) if repo.revs("%ld & merge()", draftctxs): raise error.Abort(_("cannot amend non-linear stack")) dest = draftctxs.pop(0) if dest.phase() == phases.public: raise error.Abort(_("cannot amend public changesets")) # Generate patch from wctx and apply to dest commit. mergedctx = mirrorwithmetadata(dest, "amend") wctx = repo[None] matcher = scmutil.match(wctx, pats, opts) if pats or opts else None store = patch.mempatchstore(mergedctx) backend = patch.mempatchbackend(ui, mergedctx, store) ret = patch.applydiff( ui, io.BytesIO(b"".join(list(wctx.diff(match=matcher, opts=opts)))), backend, store, ) if ret < 0: raise error.Abort( _("amend would conflict in %s") % ", ".join(backend.rejs)) memctxs = [mergedctx] mappednodes = [dest.node()] # Perform mini-rebase of our stack. for ctx in draftctxs: memctxs.append(inmemorymerge(ui, repo, ctx, memctxs[-1], ctx.p1())) mappednodes.append(ctx.node()) parentnode = None mapping = {} # Execute our list of in-memory commits, updating descendants' # parent as we go. for i, memctx in enumerate(memctxs): if i > 0: memctx = context.memctx.mirror(memctx, parentnodes=(parentnode, nullid)) parentnode = memctx.commit() mapping[mappednodes[i]] = (parentnode, ) scmutil.cleanupnodes(repo, {dest.node(): mapping.pop(dest.node())}, "amend") scmutil.cleanupnodes(repo, mapping, "rebase") with repo.dirstate.parentchange(): # Update dirstate status of amended files. repo.dirstate.rebuild(parentnode, repo[parentnode].manifest(), wctx.files(), exact=True)