def _pullbundle(repo, rev): """Find the given rev in a backup bundle and pull it back into the repository. """ other, rev = _findbundle(repo, rev) if not other: raise error.Abort( "could not find '%s' in the repo or the backup" " bundles" % rev ) lock = repo.lock() try: oldtip = len(repo) exchange.pull(repo, other, heads=[rev]) tr = repo.transaction("phase") nodes = (c.node() for c in repo.set("%d:", oldtip)) phases.retractboundary(repo, tr, 1, nodes) tr.close() finally: lock.release() if rev not in repo: raise error.Abort("unable to get rev %s from repo" % rev) return repo[rev]
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)