overlay: add support for passing errors to an external handler (bug 1379559) r?gps draft
authorbyron jones <glob@mozilla.com>
Mon, 17 Jul 2017 12:11:49 +0800
changeset 11373 293da19054d5f8da74846455f435852b880503b4
parent 11372 c8dd186ad64c6106e7c55ba45a14e9602bf8b8da
child 11374 cf98a840aafe1b04e016486faa6fb3e185208926
push id1733
push userbjones@mozilla.com
push dateWed, 19 Jul 2017 07:17:46 +0000
reviewersgps
bugs1379559
overlay: add support for passing errors to an external handler (bug 1379559) r?gps In order to deliver informative errors to those who can resolve overlay issues, add support for specifying an external command to deliver notifications. MozReview-Commit-ID: AtS9Sj0AXRp
hgext/overlay/__init__.py
hgext/overlay/tests/test-overlay-dest-state.t
--- 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)
--- a/hgext/overlay/tests/test-overlay-dest-state.t
+++ b/hgext/overlay/tests/test-overlay-dest-state.t
@@ -115,8 +115,45 @@ Metadata mismatch between source and des
   $ hg cp root subdir/foo-copy2
   $ echo 0 > subdir/foo-copy2
   $ hg commit -m 'create foo-copy2 from different source'
 
   $ hg overlay http://localhost:$HGPORT --into subdir
   0f7e081c425c already processed as 4930b59d9987; skipping 4/5 revisions
   abort: metadata mismatch for file subdir/foo-copy2 between source and dest: {'copy': 'foo-copy'} != {'copy': 'root'}
   [255]
+
+Notification
+
+  $ hg overlay http://localhost:$HGPORT --into subdir --notify 'sed "s/^/notify: /"'
+  notify: metadata mismatch for file subdir/foo-copy2 between source and dest: {'copy': 'foo-copy'} != {'copy': 'root'}
+  notify: 
+  notify: Destination Repository:
+  notify: 
+  notify: Last overlaid revision:
+  notify: 
+  notify: changeset: 4930b59d998731eedd4a01b6f3f671af0c080e36
+  notify: user:      Test User <someone@example.com>
+  notify: date:      Thu Jan 01 00:00:00 1970 +0000
+  notify: summary:   copy foo-copy to foo-copy2
+  notify: 
+  notify: Revisions that require investigation:
+  notify: 
+  notify: changeset: 5d9084d79cc3074ae45081dcb64e3473b2f55d70
+  notify: user:      Test User <someone@example.com>
+  notify: date:      Thu Jan 01 00:00:00 1970 +0000
+  notify: summary:   remove foo-copy2
+  notify: 
+  notify: changeset: 83b0c8a8cf2f5b0db017e4efab8726b722ff9d00
+  notify: user:      Test User <someone@example.com>
+  notify: date:      Thu Jan 01 00:00:00 1970 +0000
+  notify: summary:   create foo-copy2 from different source
+  0f7e081c425c already processed as 4930b59d9987; skipping 4/5 revisions
+  abort: metadata mismatch for file subdir/foo-copy2 between source and dest: {'copy': 'foo-copy'} != {'copy': 'root'}
+  [255]
+
+Bad notification switch shouldn't prevent normal errors
+
+  $ hg overlay http://localhost:$HGPORT --into subdir --notify this-command-is-bad
+  0f7e081c425c already processed as 4930b59d9987; skipping 4/5 revisions
+  notify command "this-command-is-bad" failed: [Errno 2] No such file or directory
+  abort: metadata mismatch for file subdir/foo-copy2 between source and dest: {'copy': 'foo-copy'} != {'copy': 'root'}
+  [255]