def _rewritesingle(c, _commitopts): # Predefined message overrides other message editing choices. msg = msgmap.get(c.node()) if msg is not None: _commitopts["message"] = msg _commitopts["edit"] = False if _commitopts.get("edit", False): _commitopts["message"] = ( "HG: Commit message of changeset %s\n%s" % (str(c), c.description())) bases = [ replacemap.get(c.p1().node(), c.p1().node()), replacemap.get(c.p2().node(), c.p2().node()), ] if mutation.enabled(repo): preds = [ replacemap[p] for p in mutation.predecessorsset( repo, c.node(), closest=True) if p in replacemap ] else: preds = [] newid, created = common.metarewrite(repo, c, bases, commitopts=_commitopts, copypreds=preds) if created: replacemap[c.node()] = newid
def histeditsuccessors(repo, ctx, **args): """Return all of the node's successors created as a result of histedit """ if mutation.enabled(repo): return "" asnodes = list(modifysuccessors(ctx, "histedit")) return templatekw.showlist("histeditsuccessor", asnodes, args)
def _destrestack(repo, subset, x): """restack destination for given single source revision""" unfi = repo.unfiltered() obsoleted = unfi.revs("obsolete()") getparents = unfi.changelog.parentrevs getphase = unfi._phasecache.phase nodemap = unfi.changelog.nodemap src = revset.getset(repo, subset, x).first() # Empty src or already obsoleted - Do not return a destination if not src or src in obsoleted: return smartset.baseset() # Find the obsoleted "base" by checking source's parent recursively base = src while base not in obsoleted: base = getparents(base)[0] # When encountering a public revision which cannot be obsoleted, stop # the search early and return no destination. Do the same for nullrev. if getphase(repo, base) == phases.public or base == nullrev: return smartset.baseset() # Find successors for given base # NOTE: Ideally we can use obsutil.successorssets to detect divergence # case. However it does not support cycles (unamend) well. So we use # allsuccessors and pick non-obsoleted successors manually as a workaround. basenode = repo[base].node() if mutation.enabled(repo): succnodes = mutation.allsuccessors(repo, [basenode]) else: succnodes = obsutil.allsuccessors(repo.obsstore, [basenode]) succnodes = [ n for n in succnodes if (n != basenode and n in nodemap and nodemap[n] not in obsoleted) ] # In case of a split, only keep its heads succrevs = list(unfi.revs("heads(%ln)", succnodes)) if len(succrevs) == 0: # Prune - Find the first non-obsoleted ancestor while base in obsoleted: base = getparents(base)[0] if base == nullrev: # Root node is pruned. The new base (destination) is the # virtual nullrev. return smartset.baseset([nullrev]) return smartset.baseset([base]) elif len(succrevs) == 1: # Unique visible successor case - A valid destination return smartset.baseset([succrevs[0]]) else: # Multiple visible successors - Choose the one with a greater revision # number. This is to be compatible with restack old behavior. We might # want to revisit it when we introduce the divergence concept to users. return smartset.baseset([max(succrevs)])
def smarthide(repo, revhide, revshow, local=False): """hides changecontexts and reveals some commits tries to connect related hides and shows with obs marker when reasonable and correct use local to not hide revhides without corresponding revshows """ hidectxs = repo.set(revhide) showctxs = repo.set(revshow) markers = [] nodes = [] for ctx in hidectxs: unfi = repo.unfiltered() related = set() if mutation.enabled(unfi): related.update(mutation.allpredecessors(unfi, [ctx.node()])) related.update(mutation.allsuccessors(unfi, [ctx.node()])) else: related.update(obsutil.allpredecessors(unfi.obsstore, [ctx.node()])) related.update(obsutil.allsuccessors(unfi.obsstore, [ctx.node()])) related.intersection_update(x.node() for x in showctxs) destinations = [repo[x] for x in related] # two primary objectives: # 1. correct divergence/nondivergence # 2. correct visibility of changesets for the user # secondary objectives: # 3. useful ui message in hg sl: "Undone to" # Design choices: # 1-to-1 correspondence is easy # 1-to-many correspondence is hard: # it's either divergent A to B, A to C # or split A to B,C # because of undo we don't know which # without complex logic # Solution: provide helpful ui message for # common and easy case (1 to 1), use simplest # correct solution for complex edge case if len(destinations) == 1: markers.append((ctx, destinations)) nodes.append(ctx.node()) elif len(destinations) > 1: # split markers.append((ctx, [])) nodes.append(ctx.node()) elif len(destinations) == 0: if not local: markers.append((ctx, [])) nodes.append(ctx.node()) if obsolete.isenabled(repo, obsolete.createmarkersopt): obsolete.createmarkers(repo, markers, operation="undo") visibility.remove(repo, nodes)
def singlepublicsuccessor(repo, ctx, templ, **args): """String. Get a single public successor for a given node. If there's none or more than one, return empty string. This is intended to be used for "Landed as" marking in `hg sl` output.""" if mutation.enabled(repo): return "" successorssets = obsutil.successorssets(repo, ctx.node()) unfiltered = repo.unfiltered() ctxs = (unfiltered[n] for n in itertools.chain.from_iterable(successorssets)) public = (c.hex() for c in ctxs if not c.mutable() and c != ctx) first = next(public, "") second = next(public, "") return "" if first and second else first
def unamend(ui, repo, **opts): """undo the last amend operation on the current commit Reverse the effects of an :hg:`amend` operation. Hides the current commit and checks out the previous version of the commit. :hg:`unamend` does not revert the state of the working copy, so changes that were added to the commit in the last amend operation become pending changes in the working copy. :hg:`unamend` cannot be run on amended commits that have children. In other words, you cannot unamend an amended commit in the middle of a stack. .. note:: Running :hg:`unamend` is similar to running :hg:`undo --keep` immediately after :hg:`amend`. However, unlike :hg:`undo`, which can only undo an amend if it was the last operation you performed, :hg:`unamend` can unamend any draft amended commit in the graph that does not have children. .. container:: verbose Although :hg:`unamend` is typically used to reverse the effects of :hg:`amend`, it actually rolls back the current commit to its previous version, regardless of whether the changes resulted from an :hg:`amend` operation or from another operation, such as :hg:`rebase`. """ unfi = repo # identify the commit from which to unamend curctx = repo["."] # identify the commit to which to unamend if mutation.enabled(repo): prednodes = curctx.mutationpredecessors() if not prednodes: prednodes = [] else: prednodes = [marker.prednode() for marker in predecessormarkers(curctx)] if len(prednodes) != 1: e = _("changeset must have one predecessor, found %i predecessors") raise error.Abort(e % len(prednodes)) prednode = prednodes[0] if prednode not in unfi: # Trigger autopull. autopull.trypull(unfi, [nodemod.hex(prednode)]) predctx = unfi[prednode] if curctx.children(): raise error.Abort(_("cannot unamend in the middle of a stack")) with repo.wlock(), repo.lock(): ctxbookmarks = curctx.bookmarks() changedfiles = [] wctx = repo[None] wm = wctx.manifest() cm = predctx.manifest() dirstate = repo.dirstate diff = cm.diff(wm) changedfiles.extend(pycompat.iterkeys(diff)) tr = repo.transaction("unamend") with dirstate.parentchange(): dirstate.rebuild(prednode, cm, changedfiles) # we want added and removed files to be shown # properly, not with ? and ! prefixes for filename, data in pycompat.iteritems(diff): if data[0][0] is None: dirstate.add(filename) if data[1][0] is None: dirstate.remove(filename) changes = [] for book in ctxbookmarks: changes.append((book, prednode)) repo._bookmarks.applychanges(repo, tr, changes) if obsolete.isenabled(repo, obsolete.createmarkersopt): obsolete.createmarkers(repo, [(curctx, (predctx,))]) visibility.remove(repo, [curctx.node()]) visibility.add(repo, [predctx.node()]) tr.close()
def _getscratchbranchpartsimpl( repo, peer, outgoing, confignonforwardmove, ui, bookmark, create, bookmarknode=None ): _validaterevset(repo, revsetlang.formatspec("%ln", outgoing.missing), bookmark) supportedversions = changegroup.supportedoutgoingversions(repo) # Explicitly avoid using '01' changegroup version in infinitepush to # support general delta supportedversions.discard("01") cgversion = min(supportedversions) _handlelfs(repo, outgoing.missing) cg = changegroup.makestream(repo, outgoing, cgversion, "push") params = {} params["cgversion"] = cgversion if bookmark: params["bookmark"] = bookmark if bookmarknode: params["bookmarknode"] = bookmarknode if create: params["create"] = "1" if confignonforwardmove: params["force"] = "1" parts = [] # .upper() marks this as a mandatory part: server will abort if there's no # handler parts.append( bundle2.bundlepart( constants.scratchbranchparttype.upper(), advisoryparams=pycompat.iteritems(params), data=cg, ) ) if mutation.enabled(repo): entries = mutation.entriesforbundle(repo, outgoing.missing) if entries: if constants.scratchmutationparttype not in bundle2.bundle2caps(peer): repo.ui.warn( _("no server support for %r - skipping\n") % constants.scratchmutationparttype ) else: parts.append( bundle2.bundlepart( constants.scratchmutationparttype, data=mutation.bundleentries(entries), ) ) try: treemod = extensions.find("treemanifest") remotefilelog = extensions.find("remotefilelog") sendtrees = remotefilelog.shallowbundle.cansendtrees(repo, outgoing.missing) if sendtrees != remotefilelog.shallowbundle.NoTrees: parts.append( treemod.createtreepackpart( repo, outgoing, treemod.TREEGROUP_PARTTYPE2, sendtrees=sendtrees ) ) except KeyError: pass try: snapshot = extensions.find("snapshot") except KeyError: pass else: snapshot.bundleparts.appendsnapshotmetadatabundlepart( repo, outgoing.missing, parts ) return parts
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 expushdiscoverybookmarks(pushop): repo = pushop.repo if pushop.delete: remotemarks = pushop.remote.listkeyspatterns("bookmarks", [pushop.delete]) if pushop.delete not in remotemarks: raise error.Abort( _("remote bookmark %s does not exist") % pushop.delete) pushop.outbookmarks.append( [pushop.delete, remotemarks[pushop.delete], ""]) return exchange._pushdiscoverybookmarks(pushop) if not pushop.to: ret = exchange._pushdiscoverybookmarks(pushop) if not pushop.allowanon: # check to make sure we don't push an anonymous head if pushop.revs: revs = set(pushop.revs) else: revs = set(repo.lookup(r) for r in repo.revs("head()")) revs -= set(pushop.remoteheads) # find heads that don't have a bookmark going with them for bookmark in pushop.bookmarks: rev = repo.lookup(bookmark) if rev in revs: revs.remove(rev) # remove heads that advance bookmarks (old mercurial behavior) for bookmark, old, new in pushop.outbookmarks: rev = repo.lookup(new) if rev in revs: revs.remove(rev) # we use known() instead of lookup() due to lookup throwing an # aborting error causing the connection to close anonheads = [] revs = sorted(revs) knownlist = pushop.remote.known(revs) for node, known in zip(revs, knownlist): ctx = repo[node] if (known or ctx.obsolete() or ctx.closesbranch() or # if there is a topic, let's just skip it for now (ctx.mutable() and "topic" in ctx.extra())): continue anonheads.append(short(node)) if anonheads: msg = _("push would create new anonymous heads (%s)") hint = _("use --allow-anon to override this warning") raise error.Abort(msg % ", ".join(sorted(anonheads)), hint=hint) return ret # in this path, we have a push --to command if not len(pushop.bookmarks): # if there are no bookmarks, something went wrong. bail gracefully. raise error.Abort("no bookmark found to push") bookmark = pushop.bookmarks[0] rev = pushop.revs[0] # allow new bookmark only if --create is specified old = "" remotemarks = pushop.remote.listkeyspatterns("bookmarks", [bookmark]) if bookmark in remotemarks: old = remotemarks[bookmark] elif not pushop.create: msg = _("not creating new remote bookmark") hint = _("use --create to create a new bookmark") raise error.Abort(msg, hint=hint) # allow non-fg bookmark move only if --non-forward-move is specified if not pushop.nonforwardmove and old != "": # the first check isn't technically about non-fg moves, but the non-fg # check relies on the old bm location being in the local repo if old not in repo: msg = _("remote bookmark revision is not in local repo") hint = _("pull and merge or rebase or use --non-forward-move") raise error.Abort(msg, hint=hint) if mutation.enabled(repo): foreground = mutation.foreground(repo, [repo.lookup(old)]) else: foreground = obsutil.foreground(repo, [repo.lookup(old)]) if repo[rev].node() not in foreground: msg = _("pushed rev is not in the foreground of remote bookmark") hint = _("use --non-forward-move flag to complete arbitrary moves") raise error.Abort(msg, hint=hint) if repo[old] == repo[rev]: repo.ui.status_err( _("remote bookmark already points at pushed rev\n")) return pushop.outbookmarks.append((bookmark, old, hex(rev)))
def rebasesuccessors(repo, ctx, **args): """Return all of the node's successors created as a result of rebase""" if mutation.enabled(repo): return "" rsnodes = list(modifysuccessors(ctx, "rebase")) return templatekw.showlist("rebasesuccessor", rsnodes, args)