overlay: Improve error messages during mismatch (bug 1379559) r?gps draft
authorbyron jones <glob@mozilla.com>
Wed, 12 Jul 2017 12:32:00 +0800
changeset 11372 c8dd186ad64c6106e7c55ba45a14e9602bf8b8da
parent 11359 e2a76332e0362a2aa75679b3d41d2a6224019b02
child 11373 293da19054d5f8da74846455f435852b880503b4
push id1733
push userbjones@mozilla.com
push dateWed, 19 Jul 2017 07:17:46 +0000
reviewersgps
bugs1379559
overlay: Improve error messages during mismatch (bug 1379559) r?gps Generate an error that shows the last overlaid revision and any commits that touch mismatch files. For now these error messages are not used, that's addressed in the next commit. MozReview-Commit-ID: 6ZwstNNb3R2
hgext/overlay/__init__.py
hgext/overlay/tests/test-overlay-dest-state.t
hgext/overlay/tests/test-overlay-filtered-dag.t
--- a/hgext/overlay/__init__.py
+++ b/hgext/overlay/__init__.py
@@ -31,55 +31,124 @@ testedwith = '4.1 4.2'
 cmdtable = {}
 command = cmdutil.command(cmdtable)
 
 
 REVISION_KEY = 'subtree_revision'
 SOURCE_KEY = 'subtree_source'
 
 
-def _verifymanifestsequal(sourcerepo, sourcectx, destrepo, destctx, prefix):
+def _ctx_summary(ctx):
+    return [
+        '',
+        _('changeset: %s') % ctx.hex(),
+        _('user:      %s') % ctx.user(),
+        _('date:      %s') % util.datestr(ctx.date()),
+        _('summary:   %s') % ctx.description().splitlines()[0],
+    ]
+
+
+def _summarise_changed(summary, repo_name, repo, last_ctx, prefix, files):
+    overlaid_ctx = None
+    all_ctxs = []
+    matching_ctxs = []
+
+    # Find revisions newer than the last overlaid.
+    dest_revs = scmutil.revrange(
+        repo, ['%s:: and file("path:%s")' % (last_ctx.hex(), prefix)])
+    for rev in dest_revs:
+        ctx = repo[rev]
+
+        if not overlaid_ctx:
+            overlaid_ctx = ctx
+            continue
+
+        all_ctxs.append(ctx)
+
+        # Report on revisions that touch problematic files.
+        if files and (set(ctx.files()) & files):
+            matching_ctxs.append(ctx)
+
+    # No revisions to report.
+    if not all_ctxs:
+        return
+
+    summary.extend(['', _('%s Repository:') % repo_name,
+                    '', _('Last overlaid revision:')])
+    summary.extend(_ctx_summary(overlaid_ctx))
+    summary.extend(['', _('Revisions that require investigation:')])
+
+    # 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()
+
+    summary = [error_message.rstrip()]
+    _summarise_changed(summary, _('Source'), sourcerepo, lastsourcectx,
+                       prefix, prefixed_file_set)
+    _summarise_changed(summary, _('Destination'), destrepo, lastdestctx,
+                       prefix, prefixed_file_set)
+
+    raise error.Abort(error_message, hint=hint)
+
+
+def _verifymanifestsequal(ui, sourcerepo, sourcectx, destrepo, destctx,
+                          prefix, lastsourcectx, lastdestctx):
     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:
-        raise error.Abort(_('files mismatch between source and destiation: %s')
-                          % _(', ').join(sorted(destfiles ^ sourcefiles)),
-                          hint=_('destination must match previously imported '
-                                 'changeset (%s) exactly') %
-                               short(sourcectx.node()))
+        _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()))
 
     # 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:
-            raise error.Abort(_('file flags mismatch between source and '
-                                'destination for %s: %s != %s') %
-                              (sourcepath,
-                               sourceflags or _('(none)'),
-                               destflags or _('(none)')))
+            _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)'))))
 
         # 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):
-            raise error.Abort(_('content mismatch between source (%s) '
-                                'and destination (%s) in %s') % (
-                short(sourcectx.node()), short(destctx.node()), destpath))
+            _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))
 
         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):
@@ -89,19 +158,21 @@ def _verifymanifestsequal(sourcerepo, so
         # can be influenced by the path in a parent revision's copy metadata.
         # So ignore it.
         if sourcemeta and 'copyrev' in sourcemeta:
             del sourcemeta['copyrev']
         if destmeta and 'copyrev' in destmeta:
             del destmeta['copyrev']
 
         if sourcemeta != destmeta:
-            raise error.Abort(_('metadata mismatch for file %s between source '
-                                'and dest: %s != %s') % (
-                                destpath, 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)))
 
 
 def _overlayrev(sourcerepo, sourceurl, sourcectx, destrepo, destctx,
                 prefix):
     """Overlay a single commit into another repo."""
     assert prefix.endswith('/')
     assert len(sourcectx.parents()) < 2
 
@@ -180,16 +251,17 @@ def _dooverlay(sourcerepo, sourceurl, so
 
     sourcecl = sourcerepo.changelog
     allsourcehexes = set(hex(sourcecl.node(rev)) for rev in
                          sourcecl.ancestors([sourcerevs[-1]], inclusive=True))
 
     # Attempt to find an incoming changeset in dest and prune already processed
     # source revisions.
     lastsourcectx = None
+    lastdestctx = None
     for rev in sorted(destrepo.changelog.ancestors([destctx.rev()],
                       inclusive=True), reverse=True):
         ctx = destrepo[rev]
         overlayed = ctx.extra().get(REVISION_KEY)
 
         # Changesets that weren't imported or that didn't come from the source
         # aren't important to us.
         if not overlayed or overlayed not in allsourcehexes:
@@ -198,16 +270,17 @@ def _dooverlay(sourcerepo, sourceurl, so
         lastsourcectx = sourcerepo[overlayed]
 
         # If this imported changeset is in the set scheduled for import,
         # we can prune it and all ancestors from the source set. Since
         # sourcerevs is sorted and is a single DAG head, we can simply find
         # the offset of the first seen rev and assume everything before
         # has been imported.
         try:
+            lastdestctx = ctx
             idx = sourcerevs.index(lastsourcectx.rev()) + 1
             ui.write(_('%s already processed as %s; '
                        'skipping %d/%d revisions\n' %
                        (short(lastsourcectx.node()), short(ctx.node()),
                         idx, len(sourcerevs))))
             sourcerevs = sourcerevs[idx:]
             break
         except ValueError:
@@ -255,18 +328,18 @@ def _dooverlay(sourcerepo, sourceurl, so
                 raise error.Abort(_('parent of initial source changeset does '
                                     'not match last overlayed changeset (%s)') %
                                   short(lastsourcectx.node()))
 
             comparectx = lastsourcectx
         else:
             comparectx = sourcerepo[sourcerevs[0]].p1()
 
-        _verifymanifestsequal(sourcerepo, comparectx, destrepo, destctx,
-                              prefix)
+        _verifymanifestsequal(ui, sourcerepo, comparectx, destrepo, destctx,
+                              prefix, lastsourcectx, lastdestctx)
 
     # 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,
--- a/hgext/overlay/tests/test-overlay-dest-state.t
+++ b/hgext/overlay/tests/test-overlay-dest-state.t
@@ -49,29 +49,29 @@ Addition of file in destination fails pr
   $ hg overlay http://localhost:$HGPORT --into subdir
   pulling http://localhost:$HGPORT into $TESTTMP/dest/.hg/localhost~3a* (glob)
   searching for changes
   adding changesets
   adding manifests
   adding file changes
   added 1 changesets with 2 changes to 2 files
   0f7e081c425c already processed as 4930b59d9987; skipping 4/5 revisions
-  abort: files mismatch between source and destiation: extra-file
+  abort: files mismatch between source and destination: extra-file
   (destination must match previously imported changeset (0f7e081c425c) exactly)
   [255]
 
   $ hg -q strip -r .
 
 Removal of file in destination fails precondition testing
 
   $ hg rm subdir/bar
   $ hg commit -m 'remove bar'
   $ hg overlay http://localhost:$HGPORT --into subdir
   0f7e081c425c already processed as 4930b59d9987; skipping 4/5 revisions
-  abort: files mismatch between source and destiation: bar
+  abort: files mismatch between source and destination: bar
   (destination must match previously imported changeset (0f7e081c425c) exactly)
   [255]
 
   $ hg -q strip -r .
 
 File mode difference in destination fails precondition testing
 
   $ chmod +x subdir/foo
--- a/hgext/overlay/tests/test-overlay-filtered-dag.t
+++ b/hgext/overlay/tests/test-overlay-filtered-dag.t
@@ -65,17 +65,17 @@ Incremental conversion with --noncontigu
   $ cd ../dest
 
   $ hg -q up tip
 
   $ echo 5 > prefix/foo
   $ hg commit -m 'out of band change simulating commit 5'
 
   $ hg overlay http://localhost:$HGPORT1 'not desc("FILTERED")' --into prefix
-  pulling http://localhost:$HGPORT1 into $TESTTMP/dest/.hg/localhost~3a20123
+  pulling http://localhost:$HGPORT1 into $TESTTMP/dest/.hg/localhost~3a* (glob)
   searching for changes
   adding changesets
   adding manifests
   adding file changes
   added 2 changesets with 2 changes to 1 files
   abort: source revisions must be part of contiguous DAG range
   [255]