--- a/hgext/overlay/__init__.py
+++ b/hgext/overlay/__init__.py
@@ -5,16 +5,18 @@
``hg overlay`` is used to "overlay" the changesets of a remote,
unrelated repository into a sub-directory of another.
"""
from __future__ import absolute_import
import os
+import shlex
+import subprocess
from mercurial.i18n import _
from mercurial.node import bin, hex, short
from mercurial import (
cmdutil,
context,
error,
exchange,
@@ -78,77 +80,91 @@ def _summarise_changed(summary, repo_nam
# If we didn't find any revisions that match the problematic files report
# on all revisions instead.
for ctx in matching_ctxs if matching_ctxs else all_ctxs:
summary.extend(_ctx_summary(ctx))
def _report_mismatch(ui, sourcerepo, lastsourcectx, destrepo, lastdestctx,
- prefix, files, error_message, hint=None):
- if files:
- prefixed_file_set = set('%s%s' % (prefix, f) for f in files)
- else:
- prefixed_file_set = set()
+ prefix, files, error_message, hint=None, notify=None):
+ if notify:
+ if files:
+ prefixed_file_set = set('%s%s' % (prefix, f) for f in files)
+ else:
+ prefixed_file_set = set()
- summary = [error_message.rstrip()]
- _summarise_changed(summary, _('Source'), sourcerepo, lastsourcectx,
- prefix, prefixed_file_set)
- _summarise_changed(summary, _('Destination'), destrepo, lastdestctx,
- prefix, prefixed_file_set)
+ summary = [error_message.rstrip()]
+ _summarise_changed(summary, _('Source'), sourcerepo, lastsourcectx,
+ prefix, prefixed_file_set)
+ _summarise_changed(summary, _('Destination'), destrepo, lastdestctx,
+ prefix, prefixed_file_set)
+ summary_str = ('%s\n' % '\n'.join(summary))
+
+ cmd = shlex.split(notify)
+ cmd[0] = os.path.expanduser(os.path.expandvars(cmd[0]))
+ try:
+ proc = subprocess.Popen(cmd, stdin=subprocess.PIPE)
+ proc.communicate(summary_str)
+ except OSError as ex:
+ ui.write('notify command "%s" failed: %s\n' % (cmd[0], ex))
raise error.Abort(error_message, hint=hint)
def _verifymanifestsequal(ui, sourcerepo, sourcectx, destrepo, destctx,
- prefix, lastsourcectx, lastdestctx):
+ prefix, lastsourcectx, lastdestctx, notify=None):
assert prefix.endswith('/')
sourceman = sourcectx.manifest()
destman = destctx.manifest()
sourcefiles = set(sourceman.iterkeys())
destfiles = set(p[len(prefix):] for p in destman if p.startswith(prefix))
if sourcefiles ^ destfiles:
_report_mismatch(
ui, sourcerepo, lastsourcectx, destrepo, lastdestctx, prefix,
destfiles ^ sourcefiles,
(_('files mismatch between source and destination: %s')
% ', '.join(sorted(destfiles ^ sourcefiles))),
'destination must match previously imported changeset (%s) exactly'
- % short(sourcectx.node()))
+ % short(sourcectx.node()),
+ notify=notify
+ )
# The set of paths is the same. Now verify the contents are identical.
for sourcepath, sourcenode, sourceflags in sourceman.iterentries():
destpath = '%s%s' % (prefix, sourcepath)
destnode, destflags = destman.find(destpath)
if sourceflags != destflags:
_report_mismatch(
ui, sourcerepo, lastsourcectx, destrepo, lastdestctx, prefix,
[sourcepath],
(_('file flags mismatch between source and destination for '
'%s: %s != %s') % (sourcepath, sourceflags or _('(none)'),
- destflags or _('(none)'))))
+ destflags or _('(none)'))),
+ notify=notify)
# We can't just compare the nodes because they are derived from
# content that may contain file paths in metadata, causing divergence
# between the two repos. So we compare all the content in the
# revisions.
sourcefl = sourcerepo.file(sourcepath)
destfl = destrepo.file(destpath)
if sourcefl.read(sourcenode) != destfl.read(destnode):
_report_mismatch(
ui, sourcerepo, lastsourcectx, destrepo, lastdestctx, prefix,
[sourcepath],
_('content mismatch between source (%s) and destination (%s) '
'in %s') % (short(sourcectx.node()), short(destctx.node()),
- destpath))
+ destpath),
+ notify=notify)
sourcetext = sourcefl.revision(sourcenode)
desttext = destfl.revision(destnode)
sourcemeta = filelog.parsemeta(sourcetext)[0]
destmeta = filelog.parsemeta(desttext)[0]
# Copy path needs to be normalized before comparison.
if destmeta is not None and destmeta.get('copy', '').startswith(prefix):
@@ -162,17 +178,18 @@ def _verifymanifestsequal(ui, sourcerepo
if destmeta and 'copyrev' in destmeta:
del destmeta['copyrev']
if sourcemeta != destmeta:
_report_mismatch(
ui, sourcerepo, lastsourcectx, destrepo, lastdestctx, prefix,
[sourcepath],
(_('metadata mismatch for file %s between source and dest: '
- '%s != %s') % (destpath, sourcemeta, destmeta)))
+ '%s != %s') % (destpath, sourcemeta, destmeta)),
+ notify=notify)
def _overlayrev(sourcerepo, sourceurl, sourcectx, destrepo, destctx,
prefix):
"""Overlay a single commit into another repo."""
assert prefix.endswith('/')
assert len(sourcectx.parents()) < 2
@@ -205,17 +222,17 @@ def _overlayrev(sourcerepo, sourceurl, s
memctx = context.memctx(destrepo, parents, sourcectx.description(),
files, filectxfn, user=sourcectx.user(),
date=sourcectx.date(), extra=extra)
return memctx.commit()
def _dooverlay(sourcerepo, sourceurl, sourcerevs, destrepo, destctx, prefix,
- noncontiguous):
+ noncontiguous, notify=None):
"""Overlay changesets from one repository into another.
``sourcerevs`` (iterable of revs) from ``sourcerepo`` will effectively
be replayed into ``destrepo`` on top of ``destctx``. File paths will be
added to the directory ``prefix``.
``sourcerevs`` may include revisions that have already been overlayed.
If so, overlay will resume at the first revision not yet processed.
@@ -329,17 +346,17 @@ def _dooverlay(sourcerepo, sourceurl, so
'not match last overlayed changeset (%s)') %
short(lastsourcectx.node()))
comparectx = lastsourcectx
else:
comparectx = sourcerepo[sourcerevs[0]].p1()
_verifymanifestsequal(ui, sourcerepo, comparectx, destrepo, destctx,
- prefix, lastsourcectx, lastdestctx)
+ prefix, lastsourcectx, lastdestctx, notify)
# All the validation is done. Proceed with the data conversion.
with destrepo.lock():
with destrepo.transaction('overlay'):
for i, rev in enumerate(sourcerevs):
ui.progress(_('revisions'), i + 1, total=len(sourcerevs))
sourcectx = sourcerepo[rev]
node = _overlayrev(sourcerepo, sourceurl, sourcectx,
@@ -374,19 +391,20 @@ def _mirrorrepo(ui, repo, url):
return mirrorrepo
@command('overlay', [
('d', 'dest', '', _('destination changeset on top of which to overlay '
'changesets')),
('', 'into', '', _('directory in destination in which to add files')),
('', 'noncontiguous', False, _('allow non continuous dag heads')),
+ ('', 'notify', '', _('application to handle error notifications'))
], _('[-d REV] SOURCEURL [REVS]'))
def overlay(ui, repo, sourceurl, revs=None, dest=None, into=None,
- noncontiguous=False):
+ noncontiguous=False, notify=None):
"""Integrate contents of another repository.
This command essentially replays changesets from another repository into
this one. Unlike a simple pull + rebase, the files from the remote
repository are "overlayed" or unioned with the contents of the destination
repository.
The functionality of this command is nearly identical to what ``hg
@@ -407,16 +425,20 @@ def overlay(ui, repo, sourceurl, revs=No
repository must be the single source of all changes to the destination
directory.
The restriction of states being identical is to ensure that changesets
in the source and destination are as similar as possible. For example,
if the file content in the destination did not match the source, then
the ``hg diff`` output for the next overlayed changeset would differ from
the source.
+
+ This command supports sending human readable notifications in the event
+ that an overlay failed. Set --notify to an command that handles delivery
+ of these errors. The message will be piped to the command via STDIN.
"""
# We could potentially support this later.
if not into:
raise error.Abort(_('--into must be specified'))
if not revs:
revs = 'all()'
@@ -430,9 +452,9 @@ def overlay(ui, repo, sourceurl, revs=No
destctx = repo[dest]
else:
destctx = repo['tip']
# Backdoor for testing to force static URL.
sourceurl = ui.config('overlay', 'sourceurl', sourceurl)
_dooverlay(sourcerepo, sourceurl, sourcerevs, repo, destctx, into,
- noncontiguous)
+ noncontiguous, notify)