sapling/hgext/arcdiff.py
Liubov Dmitrieva b4b2a00950 diff allow remote obsoleted revisions to be pulled if missing
Summary:
The goal is to improve backup experience, make repo after recovery to behave as it was for the most popular commands.

This is only if backup is backed by cloud sync because we have more meta information in the repo.

the commands:

`hg diff --since-last-submit`
`hg log -r 'lastsubmitted(.)'`
`hg unamend`

for example, are not working for fresh repos that are recloned because the repo will only contains the latests snapshot of commits and also all obsmarker history but not obsoleted commits.

those are popular commands, so let's make them work.

the standard error there is 'unknown revision' and it is very confusing for users

there is no reason not to pull a remote obsoleted revision into the repo if needed, and there is no reason to ask user to do that manually.

the obstore is still lazy loaded, so shouldn't be perf problems

this diff supports good range of commands, so should make experience for our backup use case users significantly better.

pulling the hidden revisions is also relatively fast and doesn't affect commit cloud state of the repo in any way

Reviewed By: quark-zju

Differential Revision: D9437518

fbshipit-source-id: 01065c642aa9a194f2d321f03c1bd747f57c74b1
2018-08-24 05:51:45 -07:00

214 lines
6.9 KiB
Python

# arcdiff.py - extension adding an option to the diff command to show changes
# since the last arcanist diff
#
# Copyright 2016 Facebook, Inc.
#
# This software may be used and distributed according to the terms of the
# GNU General Public License version 2 or any later version.
import os
from mercurial import (
commands,
error,
extensions,
hintutil,
mdiff,
patch,
registrar,
revset,
scmutil,
smartset,
)
from mercurial.i18n import _
from mercurial.node import hex
from .extlib.phabricator import arcconfig, diffprops, graphql
hint = registrar.hint()
revsetpredicate = registrar.revsetpredicate()
@hint("since-last-arc-diff")
def sincelastarcdiff():
return _("--since-last-arc-diff is deprecated, use --since-last-submit")
def extsetup(ui):
entry = extensions.wrapcommand(commands.table, "diff", _diff)
options = entry[1]
options.append(
("", "since-last-arc-diff", None, _("Deprecated alias for --since-last-submit"))
)
options.append(
("", "since-last-submit", None, _("show changes since last Phabricator submit"))
)
options.append(
(
"",
"since-last-submit-2o",
None,
_("show diff of current diff and last Phabricator submit"),
)
)
def _differentialhash(ui, repo, phabrev):
timeout = repo.ui.configint("ssl", "timeout", 5)
ca_certs = repo.ui.configpath("web", "cacerts")
try:
client = graphql.Client(repodir=repo.root, ca_bundle=ca_certs, repo=repo)
info = client.getrevisioninfo(timeout, [phabrev]).get(str(phabrev))
if not info:
return None
return info
except graphql.ClientError as e:
ui.warn(_("Error calling graphql: %s\n") % str(e))
return None
except arcconfig.ArcConfigError as e:
raise error.Abort(str(e))
def _diff2o(ui, repo, rev1, rev2, *pats, **opts):
# Phabricator revs are often filtered (hidden)
repo = repo.unfiltered()
# First reconstruct textual diffs for rev1 and rev2 independently.
def changediff(node):
nodebase = repo[node].p1().node()
m = scmutil.matchall(repo)
diff = patch.diffhunks(repo, nodebase, node, m, opts=mdiff.diffopts(context=0))
filepatches = {}
for _fctx1, _fctx2, headerlines, hunks in diff:
difflines = []
for hunkrange, hunklines in hunks:
difflines += list(hunklines)
header = patch.header(headerlines)
filepatches[header.files()[0]] = "".join(difflines)
return (set(filepatches.keys()), filepatches, node)
rev1node = repo[rev1].node()
rev2node = repo[rev2].node()
files1, filepatches1, node1 = changediff(rev1node)
files2, filepatches2, node2 = changediff(rev2node)
ui.write(_("Phabricator rev: %s\n") % hex(node1)),
ui.write(_("Local rev: %s (%s)\n") % (hex(node2), rev2))
# For files have changed, produce a diff of the diffs first using a normal
# text diff of the input diffs, then fixing-up the output for readability.
changedfiles = files1 & files2
for f in changedfiles:
opts["context"] = 0
diffopts = mdiff.diffopts(**opts)
header, hunks = mdiff.unidiff(
filepatches1[f], "", filepatches2[f], "", f, f, opts=diffopts
)
hunklines = []
for hunk in hunks:
hunklines += hunk[1]
changelines = []
i = 0
while i < len(hunklines):
line = hunklines[i]
i += 1
if line[:2] == "++":
changelines.append("+" + line[2:])
elif line[:2] == "+-":
changelines.append("-" + line[2:])
elif line[:2] == "-+":
changelines.append("-" + line[2:])
elif line[:2] == "--":
changelines.append("+" + line[2:])
elif line[:2] == "@@" or line[1:3] == "@@":
if len(changelines) < 1 or changelines[-1] != "...\n":
changelines.append("...\n")
else:
changelines.append(line)
if len(changelines):
ui.write(_("Changed: %s\n") % f)
for line in changelines:
ui.write("| " + line)
wholefilechanges = files1 ^ files2
for f in wholefilechanges:
ui.write(_("Added/removed: %s\n") % f)
def _maybepull(repo, hexrev):
if extensions.enabled().get("commitcloud", False):
repo.revs("cloudremote(%s)" % hexrev)
def _diff(orig, ui, repo, *pats, **opts):
if (
not opts.get("since_last_submit")
and not opts.get("since_last_arc_diff")
and not opts.get("since_last_submit_2o")
):
return orig(ui, repo, *pats, **opts)
if opts.get("since_last_arc_diff"):
hintutil.trigger("since-last-arc-diff")
if len(opts["rev"]) > 1:
mess = _("cannot specify --since-last-arc-diff with multiple revisions")
raise error.Abort(mess)
try:
targetrev = opts["rev"][0]
except IndexError:
targetrev = "."
ctx = repo[targetrev]
phabrev = diffprops.parserevfromcommitmsg(ctx.description())
if phabrev is None:
mess = _("local changeset is not associated with a differential " "revision")
raise error.Abort(mess)
rev = _differentialhash(ui, repo, phabrev)
if rev is None or not isinstance(rev, dict) or "hash" not in rev:
mess = _("unable to determine previous changeset hash")
raise error.Abort(mess)
rev = str(rev["hash"])
_maybepull(repo, rev)
opts["rev"] = [rev, targetrev]
# if patterns aren't provided, restrict diff to files in both changesets
# this prevents performing a diff on rebased changes
if len(pats) == 0:
prev = set(repo.unfiltered()[rev].files())
curr = set(repo[targetrev].files())
pats = tuple(os.path.join(repo.root, p) for p in prev | curr)
if opts.get("since_last_submit_2o"):
return _diff2o(ui, repo, rev, ".", **opts)
else:
return orig(ui, repo.unfiltered(), *pats, **opts)
@revsetpredicate("lastsubmitted(set)")
def lastsubmitted(repo, subset, x):
revs = revset.getset(repo, revset.fullreposet(repo), x)
phabrevs = set()
for rev in revs:
phabrev = diffprops.parserevfromcommitmsg(repo[rev].description())
if phabrev is None:
mess = _("local changeset is not associated with a differential revision")
raise error.Abort(mess)
phabrevs.add(phabrev)
resultrevs = set()
for phabrev in phabrevs:
diffrev = _differentialhash(repo.ui, repo, phabrev)
if diffrev is None or not isinstance(diffrev, dict) or "hash" not in diffrev:
mess = _("unable to determine previous changeset hash")
raise error.Abort(mess)
lasthash = str(diffrev["hash"])
_maybepull(repo, lasthash)
resultrevs.add(repo.unfiltered()[lasthash])
return subset & smartset.baseset(sorted(resultrevs))