branching: merge stable into default
authorPierre-Yves David <pierre-yves.david@octobus.net>
Fri, 03 Aug 2018 01:16:51 +0200
changeset 3937 544f394997aa
parent 3918 66a863bb6401 (current diff)
parent 3932 35b2d201eb71 (diff)
child 3942 38c9a050ba03
branching: merge stable into default
--- a/hgext3rd/evolve/__init__.py	Wed Aug 01 23:52:10 2018 +0200
+++ b/hgext3rd/evolve/__init__.py	Fri Aug 03 01:16:51 2018 +0200
@@ -1409,9 +1409,10 @@
     cmdutil.afterresolvedstates.append(afterresolved)
     cmdutil.afterresolvedstates.append(grabresolved)
 
-    statedata = ('evolve', cmdutil.fileexistspredicate('evolvestate'),
-                 _evolvemessage)
-    cmdutil.STATES = (statedata, ) + cmdutil.STATES
+    if util.safehasattr(cmdutil, 'STATES'):
+        statedata = ('evolve', cmdutil.fileexistspredicate('evolvestate'),
+                     _evolvemessage)
+        cmdutil.STATES = (statedata, ) + cmdutil.STATES
 
 @eh.wrapfunction(hg, 'clean')
 def clean(orig, repo, *args, **kwargs):
--- a/hgext3rd/evolve/compat.py	Wed Aug 01 23:52:10 2018 +0200
+++ b/hgext3rd/evolve/compat.py	Fri Aug 03 01:16:51 2018 +0200
@@ -11,6 +11,7 @@
 from mercurial import (
     context,
     copies,
+    encoding,
     mdiff,
     obsolete,
     obsutil,
@@ -28,11 +29,13 @@
     changesetdisplayer = logcmdutil.changesetdisplayer
     changesetprinter = logcmdutil.changesetprinter
     displaygraph = logcmdutil.displaygraph
+    changesetdiffer = logcmdutil.changesetdiffer
 except (AttributeError, ImportError):
     from mercurial import cmdutil
     changesetdisplayer = cmdutil.show_changeset
     changesetprinter = cmdutil.changeset_printer
     displaygraph = cmdutil.displaygraph
+    changesetdiffer = None
 
 from . import (
     exthelper,
@@ -451,5 +454,333 @@
 
     return copy, movewithdir, diverge, renamedelete, dirmove
 
+# code imported from Mercurial core at 4.3 + patch
+def fixoldmergecopies(repo, c1, c2, base):
+
+    from mercurial import pathutil
+
+    # avoid silly behavior for update from empty dir
+    if not c1 or not c2 or c1 == c2:
+        return {}, {}, {}, {}, {}
+
+    # avoid silly behavior for parent -> working dir
+    if c2.node() is None and c1.node() == repo.dirstate.p1():
+        return repo.dirstate.copies(), {}, {}, {}, {}
+
+    # Copy trace disabling is explicitly below the node == p1 logic above
+    # because the logic above is required for a simple copy to be kept across a
+    # rebase.
+    if repo.ui.configbool('experimental', 'disablecopytrace'):
+        return {}, {}, {}, {}, {}
+
+    # In certain scenarios (e.g. graft, update or rebase), base can be
+    # overridden We still need to know a real common ancestor in this case We
+    # can't just compute _c1.ancestor(_c2) and compare it to ca, because there
+    # can be multiple common ancestors, e.g. in case of bidmerge.  Because our
+    # caller may not know if the revision passed in lieu of the CA is a genuine
+    # common ancestor or not without explicitly checking it, it's better to
+    # determine that here.
+    #
+    # base.descendant(wc) and base.descendant(base) are False, work around that
+    _c1 = c1.p1() if c1.rev() is None else c1
+    _c2 = c2.p1() if c2.rev() is None else c2
+    # an endpoint is "dirty" if it isn't a descendant of the merge base
+    # if we have a dirty endpoint, we need to trigger graft logic, and also
+    # keep track of which endpoint is dirty
+    dirtyc1 = not (base == _c1 or base.descendant(_c1))
+    dirtyc2 = not (base == _c2 or base.descendant(_c2))
+    graft = dirtyc1 or dirtyc2
+    tca = base
+    if graft:
+        tca = _c1.ancestor(_c2)
+
+    limit = copies._findlimit(repo, c1.rev(), c2.rev())
+    if limit is None:
+        # no common ancestor, no copies
+        return {}, {}, {}, {}, {}
+    repo.ui.debug("  searching for copies back to rev %d\n" % limit)
+
+    m1 = c1.manifest()
+    m2 = c2.manifest()
+    mb = base.manifest()
+
+    # gather data from _checkcopies:
+    # - diverge = record all diverges in this dict
+    # - copy = record all non-divergent copies in this dict
+    # - fullcopy = record all copies in this dict
+    # - incomplete = record non-divergent partial copies here
+    # - incompletediverge = record divergent partial copies here
+    diverge = {} # divergence data is shared
+    incompletediverge = {}
+    data1 = {'copy': {},
+             'fullcopy': {},
+             'incomplete': {},
+             'diverge': diverge,
+             'incompletediverge': incompletediverge,
+            }
+    data2 = {'copy': {},
+             'fullcopy': {},
+             'incomplete': {},
+             'diverge': diverge,
+             'incompletediverge': incompletediverge,
+            }
+
+    # find interesting file sets from manifests
+    addedinm1 = m1.filesnotin(mb)
+    addedinm2 = m2.filesnotin(mb)
+    bothnew = sorted(addedinm1 & addedinm2)
+    if tca == base:
+        # unmatched file from base
+        u1r, u2r = copies._computenonoverlap(repo, c1, c2, addedinm1, addedinm2)
+        u1u, u2u = u1r, u2r
+    else:
+        # unmatched file from base (DAG rotation in the graft case)
+        u1r, u2r = copies._computenonoverlap(repo, c1, c2, addedinm1, addedinm2,
+                                             baselabel='base')
+        # unmatched file from topological common ancestors (no DAG rotation)
+        # need to recompute this for directory move handling when grafting
+        mta = tca.manifest()
+        u1u, u2u = copies._computenonoverlap(repo, c1, c2, m1.filesnotin(mta),
+                                             m2.filesnotin(mta),
+                                             baselabel='topological common ancestor')
+
+    for f in u1u:
+        copies._checkcopies(c1, c2, f, base, tca, dirtyc1, limit, data1)
+
+    for f in u2u:
+        copies._checkcopies(c2, c1, f, base, tca, dirtyc2, limit, data2)
+
+    copy = dict(data1['copy'])
+    copy.update(data2['copy'])
+    fullcopy = dict(data1['fullcopy'])
+    fullcopy.update(data2['fullcopy'])
+
+    if dirtyc1:
+        copies._combinecopies(data2['incomplete'], data1['incomplete'], copy, diverge,
+                              incompletediverge)
+    else:
+        copies._combinecopies(data1['incomplete'], data2['incomplete'], copy, diverge,
+                              incompletediverge)
+
+    renamedelete = {}
+    renamedeleteset = set()
+    divergeset = set()
+    for of, fl in diverge.items():
+        if len(fl) == 1 or of in c1 or of in c2:
+            del diverge[of] # not actually divergent, or not a rename
+            if of not in c1 and of not in c2:
+                # renamed on one side, deleted on the other side, but filter
+                # out files that have been renamed and then deleted
+                renamedelete[of] = [f for f in fl if f in c1 or f in c2]
+                renamedeleteset.update(fl) # reverse map for below
+        else:
+            divergeset.update(fl) # reverse map for below
+
+    if bothnew:
+        repo.ui.debug("  unmatched files new in both:\n   %s\n"
+                      % "\n   ".join(bothnew))
+    bothdiverge = {}
+    bothincompletediverge = {}
+    remainder = {}
+    both1 = {'copy': {},
+             'fullcopy': {},
+             'incomplete': {},
+             'diverge': bothdiverge,
+             'incompletediverge': bothincompletediverge
+            }
+    both2 = {'copy': {},
+             'fullcopy': {},
+             'incomplete': {},
+             'diverge': bothdiverge,
+             'incompletediverge': bothincompletediverge
+            }
+    for f in bothnew:
+        copies._checkcopies(c1, c2, f, base, tca, dirtyc1, limit, both1)
+        copies._checkcopies(c2, c1, f, base, tca, dirtyc2, limit, both2)
+    if dirtyc1 and dirtyc2:
+        pass
+    elif dirtyc1:
+        # incomplete copies may only be found on the "dirty" side for bothnew
+        assert not both2['incomplete']
+        remainder = copies._combinecopies({}, both1['incomplete'], copy, bothdiverge,
+                                          bothincompletediverge)
+    elif dirtyc2:
+        assert not both1['incomplete']
+        remainder = copies._combinecopies({}, both2['incomplete'], copy, bothdiverge,
+                                          bothincompletediverge)
+    else:
+        # incomplete copies and divergences can't happen outside grafts
+        assert not both1['incomplete']
+        assert not both2['incomplete']
+        assert not bothincompletediverge
+    for f in remainder:
+        assert f not in bothdiverge
+        ic = remainder[f]
+        if ic[0] in (m1 if dirtyc1 else m2):
+            # backed-out rename on one side, but watch out for deleted files
+            bothdiverge[f] = ic
+    for of, fl in bothdiverge.items():
+        if len(fl) == 2 and fl[0] == fl[1]:
+            copy[fl[0]] = of # not actually divergent, just matching renames
+
+    if fullcopy and repo.ui.debugflag:
+        repo.ui.debug("  all copies found (* = to merge, ! = divergent, "
+                      "% = renamed and deleted):\n")
+        for f in sorted(fullcopy):
+            note = ""
+            if f in copy:
+                note += "*"
+            if f in divergeset:
+                note += "!"
+            if f in renamedeleteset:
+                note += "%"
+            repo.ui.debug("   src: '%s' -> dst: '%s' %s\n" % (fullcopy[f], f,
+                                                              note))
+    del divergeset
+
+    if not fullcopy:
+        return copy, {}, diverge, renamedelete, {}
+
+    repo.ui.debug("  checking for directory renames\n")
+
+    # generate a directory move map
+    d1, d2 = c1.dirs(), c2.dirs()
+    # Hack for adding '', which is not otherwise added, to d1 and d2
+    d1.addpath('/')
+    d2.addpath('/')
+    invalid = set()
+    dirmove = {}
+
+    # examine each file copy for a potential directory move, which is
+    # when all the files in a directory are moved to a new directory
+    for dst, src in fullcopy.iteritems():
+        dsrc, ddst = pathutil.dirname(src), pathutil.dirname(dst)
+        if dsrc in invalid:
+            # already seen to be uninteresting
+            continue
+        elif dsrc in d1 and ddst in d1:
+            # directory wasn't entirely moved locally
+            invalid.add(dsrc + "/")
+        elif dsrc in d2 and ddst in d2:
+            # directory wasn't entirely moved remotely
+            invalid.add(dsrc + "/")
+        elif dsrc + "/" in dirmove and dirmove[dsrc + "/"] != ddst + "/":
+            # files from the same directory moved to two different places
+            invalid.add(dsrc + "/")
+        else:
+            # looks good so far
+            dirmove[dsrc + "/"] = ddst + "/"
+
+    for i in invalid:
+        if i in dirmove:
+            del dirmove[i]
+    del d1, d2, invalid
+
+    if not dirmove:
+        return copy, {}, diverge, renamedelete, {}
+
+    for d in dirmove:
+        repo.ui.debug("   discovered dir src: '%s' -> dst: '%s'\n" %
+                      (d, dirmove[d]))
+
+    movewithdir = {}
+    # check unaccounted nonoverlapping files against directory moves
+    for f in u1r + u2r:
+        if f not in fullcopy:
+            for d in dirmove:
+                if f.startswith(d):
+                    # new file added in a directory that was moved, move it
+                    df = dirmove[d] + f[len(d):]
+                    if df not in copy:
+                        movewithdir[f] = df
+                        repo.ui.debug(("   pending file src: '%s' -> "
+                                       "dst: '%s'\n") % (f, df))
+                    break
+
+    return copy, movewithdir, diverge, renamedelete, dirmove
+
 if util.safehasattr(copies, '_fullcopytracing'):
     copies._fullcopytracing = fixedcopytracing
+elif util.safehasattr(copies, 'mergecopies'):
+    # compat fix for hg <= 4.3
+    copies.mergecopies = fixoldmergecopies
+
+if not util.safehasattr(obsutil, "_succs"):
+    class _succs(list):
+        """small class to represent a successors with some metadata about it"""
+
+        def __init__(self, *args, **kwargs):
+            super(_succs, self).__init__(*args, **kwargs)
+            self.markers = set()
+
+        def copy(self):
+            new = _succs(self)
+            new.markers = self.markers.copy()
+            return new
+
+        @util.propertycache
+        def _set(self):
+            # immutable
+            return set(self)
+
+        def canmerge(self, other):
+            return self._set.issubset(other._set)
+else:
+    from mercurial.obsutil import _succs
+
+def wrap_succs(succs):
+    """ Wrap old data format of successorsets (tuple) only if if's not yet a
+    _succs instance
+    """
+
+    if not util.safehasattr(succs, "markers"):
+        return _succs(succs)
+    else:
+        return succs
+
+if not util.safehasattr(obsutil, "markersdates"):
+    MARKERS_DATE_COMPAT = True
+else:
+    MARKERS_DATE_COMPAT = False
+
+def markersdates(markers):
+    """returns the list of dates for a list of markers
+    """
+    if MARKERS_DATE_COMPAT is False:
+        return obsutil.markersdates(markers)
+
+    return [m[4] for m in markers]
+
+if not util.safehasattr(obsutil, "markersusers"):
+    MARKERS_USERS_COMPAT = True
+else:
+    MARKERS_USERS_COMPAT = False
+
+def markersusers(markers):
+    """ Returns a sorted list of markers users without duplicates
+    """
+    if MARKERS_USERS_COMPAT is False:
+        return obsutil.markersusers(markers)
+
+    markersmeta = [dict(m[3]) for m in markers]
+    users = set(encoding.tolocal(meta['user']) for meta in markersmeta
+                if meta.get('user'))
+
+    return sorted(users)
+
+if not util.safehasattr(obsutil, "markersoperations"):
+    MARKERS_OPERATIONS_COMPAT = True
+else:
+    MARKERS_OPERATIONS_COMPAT = False
+
+def markersoperations(markers):
+    """ Returns a sorted list of markers operations without duplicates
+    """
+    if MARKERS_OPERATIONS_COMPAT is False:
+        return obsutil.markersoperations(markers)
+
+    markersmeta = [dict(m[3]) for m in markers]
+    operations = set(meta.get('operation') for meta in markersmeta
+                     if meta.get('operation'))
+
+    return sorted(operations)
--- a/hgext3rd/evolve/evolvecmd.py	Wed Aug 01 23:52:10 2018 +0200
+++ b/hgext3rd/evolve/evolvecmd.py	Fri Aug 03 01:16:51 2018 +0200
@@ -707,7 +707,10 @@
                     " content-divergent changesets.\nHG: Resolve conflicts"
                     " in commit messages to continue.\n\n")
 
-        resolveddesc = ui.edit(prefixes + desc, ui.username(), action='desc')
+        if 5 <= len(ui.edit.im_func.func_defaults): # <= hg-4.3
+            resolveddesc = ui.edit(prefixes + desc, ui.username(), action='desc')
+        else:
+            resolveddesc = ui.edit(prefixes + desc, ui.username())
         # make sure we remove the prefixes part from final commit message
         if prefixes in resolveddesc:
             # hack, we should find something better
--- a/hgext3rd/evolve/obshistory.py	Wed Aug 01 23:52:10 2018 +0200
+++ b/hgext3rd/evolve/obshistory.py	Fri Aug 03 01:16:51 2018 +0200
@@ -107,7 +107,7 @@
     fullsuccessorsets = [] # successor set + markers
     for sset in ssets:
         if sset:
-            fullsuccessorsets.append(sset)
+            fullsuccessorsets.append(compat.wrap_succs(sset))
         else:
             # successorsset return an empty set() when ctx or one of its
             # successors is pruned.
@@ -123,11 +123,11 @@
             for mark in succsmap.get(ctx.node(), ()):
                 if not mark[1]:
                     foundany = True
-                    sset = obsutil._succs()
+                    sset = compat._succs()
                     sset.markers.add(mark)
                     fullsuccessorsets.append(sset)
             if not foundany:
-                fullsuccessorsets.append(obsutil._succs())
+                fullsuccessorsets.append(compat._succs())
 
     values = []
     for sset in fullsuccessorsets:
@@ -141,8 +141,16 @@
     We display the node, description (if available) and various information
     about obsolescence markers affecting it"""
 
-    def __init__(self, ui, repo, differ=None, diffopts=None, buffered=False):
-        super(obsmarker_printer, self).__init__(ui, repo, differ=differ, diffopts=diffopts, buffered=buffered)
+    def __init__(self, ui, repo, *args, **kwargs):
+
+        if kwargs.pop('obspatch', False):
+            if compat.changesetdiffer is None:
+                kwargs['matchfn'] = scmutil.matchall(repo)
+            else:
+                kwargs['differ'] = scmutil.matchall(repo)
+
+        super(obsmarker_printer, self).__init__(ui, repo, *args, **kwargs)
+        diffopts = kwargs.get('diffopts', {})
 
         # Compat 4.6
         if not util.safehasattr(self, "_includediff"):
@@ -415,11 +423,8 @@
     return sorted(seen), nodesucc, nodeprec
 
 def _debugobshistorygraph(ui, repo, revs, opts):
-    matchfn = None
-    if opts.get('patch'):
-        matchfn = scmutil.matchall(repo)
 
-    displayer = obsmarker_printer(ui, repo.unfiltered(), matchfn, opts, buffered=True)
+    displayer = obsmarker_printer(ui, repo.unfiltered(), obspatch=True, diffopts=opts, buffered=True)
     edges = graphmod.asciiedges
     walker = _obshistorywalker(repo.unfiltered(), revs, opts.get('all', False), opts.get('filternonlocal', False))
     compat.displaygraph(ui, repo, walker, displayer, edges)
@@ -668,7 +673,7 @@
                  label="evolve.node")
 
     # Operations
-    operations = obsutil.markersoperations(markers)
+    operations = compat.markersoperations(markers)
     if operations:
         fm.plain(' using ')
         fm.write('operation', '%s', ", ".join(operations), label="evolve.operation")
@@ -676,13 +681,13 @@
     fm.plain(' by ')
 
     # Users
-    users = obsutil.markersusers(markers)
+    users = compat.markersusers(markers)
     fm.write('user', '%s', ", ".join(users),
              label="evolve.user")
     fm.plain(' ')
 
     # Dates
-    dates = obsutil.markersdates(markers)
+    dates = compat.markersdates(markers)
     if dates:
         min_date = min(dates)
         max_date = max(dates)
--- a/hgext3rd/topic/__init__.py	Wed Aug 01 23:52:10 2018 +0200
+++ b/hgext3rd/topic/__init__.py	Fri Aug 03 01:16:51 2018 +0200
@@ -487,8 +487,9 @@
                 empty = csetcount == 0
                 if empty and not ctwasempty:
                     ui.status('active topic %r is now empty\n' % ct)
-                    if ('phase' in tr.names
-                            or any(n.startswith('push-response') for n in tr.names)):
+                    if ('phase' in getattr(tr, 'names', ())
+                            or any(n.startswith('push-response')
+                                   for n in getattr(tr, 'names', ()))):
                         ui.status(_("(use 'hg topic --clear' to clear it if needed)\n"))
                 hint = _("(see 'hg help topics' for more information)\n")
                 if ctwasempty and not empty: