compat.py 16.2 KB
Newer Older
1
2
3
4
5
6
7
8
# Copyright 2017 Octobus <contact@octobus.net>
#
# This software may be used and distributed according to the terms of the
# GNU General Public License version 2 or any later version.
"""
Compatibility module
"""

9
10
import array
import contextlib
11

12
from mercurial import (
13
    cmdutil,
14
    context,
15
    copies,
16
    hg,
17
    logcmdutil,
18
    merge as mergemod,
19
    obsolete,
20
    pycompat,
21
    registrar,
22
23
    repair,
    scmutil,
24
    util,
25
    ui as uimod,
26
27
)

28
29
30
31
32
33
34
if pycompat.ispy3:
    arraytobytes = array.array.tobytes
    arrayfrombytes = array.array.frombytes
else:
    arraytobytes = array.array.tostring
    arrayfrombytes = array.array.fromstring

35
# hg <= 5.2 (c21aca51b392)
36
37
38
39
try:
    from mercurial import pathutil
    dirs = pathutil.dirs
except (AttributeError, ImportError):
40
    dirs = util.dirs  # pytype: disable=module-attr
41

42
43
44
45
46
47
48
# hg <= 5.4 (b7808443ed6a)
try:
    from mercurial import mergestate as mergestatemod
    mergestate = mergestatemod.mergestate
except (AttributeError, ImportError):
    mergestate = mergemod.mergestate  # pytype: disable=module-attr

49
50
51
52
53
54
from . import (
    exthelper,
)

eh = exthelper.exthelper()

55
56
# Evolution renaming compat

57
TROUBLES = {
58
59
60
    r'ORPHAN': b'orphan',
    r'CONTENTDIVERGENT': b'content-divergent',
    r'PHASEDIVERGENT': b'phase-divergent',
61
}
62

63
# hg <= 4.6 (bec1212eceaa)
64
if util.safehasattr(uimod.ui, 'makeprogress'):
65
    def progress(ui, topic, pos, item=b"", unit=b"", total=None):
66
67
68
69
70
71
        progress = ui.makeprogress(topic, unit, total)
        if pos is not None:
            progress.update(pos, item=item)
        else:
            progress.complete()
else:
72
    def progress(ui, topic, pos, item=b"", unit=b"", total=None):
73
        ui.progress(topic, pos, item, unit, total)
74

75
# XXX: Better detection of property cache
76
if r'predecessors' not in dir(obsolete.obsstore):
77
78
79
80
81
    @property
    def predecessors(self):
        return self.precursors

    obsolete.obsstore.predecessors = predecessors
82

83
84
def memfilectx(repo, ctx, fctx, flags, copied, path):
    # XXX Would it be better at the module level?
85
    varnames = context.memfilectx.__init__.__code__.co_varnames  # pytype: disable=attribute-error
86

87
    if r"copysource" in varnames:
88
        mctx = context.memfilectx(repo, ctx, fctx.path(), fctx.data(),
89
90
                                  islink=b'l' in flags,
                                  isexec=b'x' in flags,
91
                                  copysource=copied.get(path))
92
    # hg <= 4.9 (550a172a603b)
93
    elif varnames[2] == r"changectx":
94
        mctx = context.memfilectx(repo, ctx, fctx.path(), fctx.data(),
95
96
                                  islink=b'l' in flags,
                                  isexec=b'x' in flags,
97
                                  copied=copied.get(path))  # pytype: disable=wrong-keyword-args
98
    return mctx
99

100
try:
101
    # hg <= 4.6 (46c2b19a1263)
102
    bmrevset = repair.stripbmrevset  # pytype: disable=module-attr
103
104
105
except AttributeError:
    bmrevset = scmutil.bookmarkrevs

106
hg48 = util.safehasattr(copies, 'stringutil')
107
108
109
110
111
# code imported from Mercurial core at ae17555ef93f + patch
def fixedcopytracing(repo, c1, c2, base):
    """A complete copy-patse of copies._fullcopytrace with a one line fix to
    handle when the base is not parent of both c1 and c2. This should be
    converted in a compat function once https://phab.mercurial-scm.org/D3896
112
    gets in and once we drop support for 4.6, this should be removed."""
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129

    from mercurial import pathutil

    # 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.isancestorof(wc) is 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
130
131
132
    if util.safehasattr(base, 'isancestorof'):
        dirtyc1 = not base.isancestorof(_c1)
        dirtyc2 = not base.isancestorof(_c2)
133
    else: # hg <= 4.6 (fbec9c0b32d3)
134
135
        dirtyc1 = not base.descendant(_c1)
        dirtyc2 = not base.descendant(_c2)
136
137
138
139
140
    graft = dirtyc1 or dirtyc2
    tca = base
    if graft:
        tca = _c1.ancestor(_c2)

141
    # hg <= 4.9 (dc50121126ae)
142
    try:
143
        limit = copies._findlimit(repo, c1, c2)  # pytype: disable=module-attr
144
    except (AttributeError, TypeError):
145
        limit = copies._findlimit(repo, c1.rev(), c2.rev())  # pytype: disable=module-attr
146
147
148
    if limit is None:
        # no common ancestor, no copies
        return {}, {}, {}, {}, {}
149
    repo.ui.debug(b"  searching for copies back to rev %d\n" % limit)
150
151
152
153
154
155
156
157
158
159
160
161
162

    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 = {}
163
164
165
166
167
168
169
170
171
172
173
174
    data1 = {b'copy': {},
             b'fullcopy': {},
             b'incomplete': {},
             b'diverge': diverge,
             b'incompletediverge': incompletediverge,
             }
    data2 = {b'copy': {},
             b'fullcopy': {},
             b'incomplete': {},
             b'diverge': diverge,
             b'incompletediverge': incompletediverge,
             }
175
176

    # find interesting file sets from manifests
177
178
179
180
181
182
    if hg48:
        addedinm1 = m1.filesnotin(mb, repo.narrowmatch())
        addedinm2 = m2.filesnotin(mb, repo.narrowmatch())
    else:
        addedinm1 = m1.filesnotin(mb)
        addedinm2 = m2.filesnotin(mb)
183
184
185
    bothnew = sorted(addedinm1 & addedinm2)
    if tca == base:
        # unmatched file from base
186
        u1r, u2r = copies._computenonoverlap(repo, c1, c2, addedinm1, addedinm2)  # pytype: disable=module-attr
187
188
189
        u1u, u2u = u1r, u2r
    else:
        # unmatched file from base (DAG rotation in the graft case)
190
        u1r, u2r = copies._computenonoverlap(repo, c1, c2, addedinm1, addedinm2,  # pytype: disable=module-attr
191
                                             baselabel=b'base')
192
193
194
        # unmatched file from topological common ancestors (no DAG rotation)
        # need to recompute this for directory move handling when grafting
        mta = tca.manifest()
195
196
197
        if hg48:
            m1f = m1.filesnotin(mta, repo.narrowmatch())
            m2f = m2.filesnotin(mta, repo.narrowmatch())
198
            baselabel = b'topological common ancestor'
199
            u1u, u2u = copies._computenonoverlap(repo, c1, c2, m1f, m2f,  # pytype: disable=module-attr
200
201
                                                 baselabel=baselabel)
        else:
202
            u1u, u2u = copies._computenonoverlap(repo, c1, c2, m1.filesnotin(mta),  # pytype: disable=module-attr
203
                                                 m2.filesnotin(mta),
204
                                                 baselabel=b'topological common ancestor')
205
206

    for f in u1u:
207
        copies._checkcopies(c1, c2, f, base, tca, dirtyc1, limit, data1)  # pytype: disable=module-attr
208
209

    for f in u2u:
210
        copies._checkcopies(c2, c1, f, base, tca, dirtyc2, limit, data2)  # pytype: disable=module-attr
211

212
213
214
215
    copy = dict(data1[b'copy'])
    copy.update(data2[b'copy'])
    fullcopy = dict(data1[b'fullcopy'])
    fullcopy.update(data2[b'fullcopy'])
216
217

    if dirtyc1:
218
        copies._combinecopies(data2[b'incomplete'], data1[b'incomplete'], copy, diverge,  # pytype: disable=module-attr
219
220
                              incompletediverge)
    else:
221
        copies._combinecopies(data1[b'incomplete'], data2[b'incomplete'], copy, diverge,  # pytype: disable=module-attr
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
                              incompletediverge)

    renamedelete = {}
    renamedeleteset = set()
    divergeset = set()
    for of, fl in list(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:
239
240
        repo.ui.debug(b"  unmatched files new in both:\n   %s\n"
                      % b"\n   ".join(bothnew))
241
242
243
    bothdiverge = {}
    bothincompletediverge = {}
    remainder = {}
244
245
246
247
248
249
250
251
252
253
254
255
    both1 = {b'copy': {},
             b'fullcopy': {},
             b'incomplete': {},
             b'diverge': bothdiverge,
             b'incompletediverge': bothincompletediverge
             }
    both2 = {b'copy': {},
             b'fullcopy': {},
             b'incomplete': {},
             b'diverge': bothdiverge,
             b'incompletediverge': bothincompletediverge
             }
256
    for f in bothnew:
257
258
        copies._checkcopies(c1, c2, f, base, tca, dirtyc1, limit, both1)  # pytype: disable=module-attr
        copies._checkcopies(c2, c1, f, base, tca, dirtyc2, limit, both2)  # pytype: disable=module-attr
259
260
261
262
263

    if dirtyc1 and dirtyc2:
        pass
    elif dirtyc1:
        # incomplete copies may only be found on the "dirty" side for bothnew
264
        assert not both2[b'incomplete']
265
        remainder = copies._combinecopies({}, both1[b'incomplete'], copy, bothdiverge,  # pytype: disable=module-attr
266
267
                                          bothincompletediverge)
    elif dirtyc2:
268
        assert not both1[b'incomplete']
269
        remainder = copies._combinecopies({}, both2[b'incomplete'], copy, bothdiverge,  # pytype: disable=module-attr
270
271
272
                                          bothincompletediverge)
    else:
        # incomplete copies and divergences can't happen outside grafts
273
274
        assert not both1[b'incomplete']
        assert not both2[b'incomplete']
275
276
277
278
279
280
281
282
283
284
285
286
        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:
287
288
        repo.ui.debug(b"  all copies found (* = to merge, ! = divergent, "
                      b"% = renamed and deleted):\n")
289
        for f in sorted(fullcopy):
290
            note = b""
291
            if f in copy:
292
                note += b"*"
293
            if f in divergeset:
294
                note += b"!"
295
            if f in renamedeleteset:
296
297
298
                note += b"%"
            repo.ui.debug(b"   src: '%s' -> dst: '%s' %s\n" % (fullcopy[f], f,
                                                               note))
299
300
301
302
303
    del divergeset

    if not fullcopy:
        return copy, {}, diverge, renamedelete, {}

304
    repo.ui.debug(b"  checking for directory renames\n")
305
306
307
308

    # generate a directory move map
    d1, d2 = c1.dirs(), c2.dirs()
    # Hack for adding '', which is not otherwise added, to d1 and d2
309
310
    d1.addpath(b'/')
    d2.addpath(b'/')
311
312
313
314
315
    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
316
    for dst, src in fullcopy.items():
317
318
319
320
321
322
        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
323
            invalid.add(dsrc + b"/")
324
325
        elif dsrc in d2 and ddst in d2:
            # directory wasn't entirely moved remotely
326
327
            invalid.add(dsrc + b"/")
        elif dsrc + b"/" in dirmove and dirmove[dsrc + b"/"] != ddst + b"/":
328
            # files from the same directory moved to two different places
329
            invalid.add(dsrc + b"/")
330
331
        else:
            # looks good so far
332
            dirmove[dsrc + b"/"] = ddst + b"/"
333
334
335
336
337
338
339
340
341
342

    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:
343
        repo.ui.debug(b"   discovered dir src: '%s' -> dst: '%s'\n" %
344
345
346
347
348
349
350
351
352
353
354
355
                      (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
356
357
                        repo.ui.debug((b"   pending file src: '%s' -> "
                                       b"dst: '%s'\n") % (f, df))
358
359
360
361
                    break

    return copy, movewithdir, diverge, renamedelete, dirmove

362
# hg <= 4.9 (7694b685bb10)
363
fixupstreamed = util.safehasattr(scmutil, 'movedirstate')
364
if not fixupstreamed:
365
    copies._fullcopytracing = fixedcopytracing
366

367
368
369
370
371
372
373
374
# help category compatibility
# hg <= 4.7 (c303d65d2e34)
def helpcategorykwargs(categoryname):
    """Backwards-compatible specification of the helpategory argument."""
    category = getattr(registrar.command, categoryname, None)
    if not category:
        return {}
    return {'helpcategory': category}
375
376

# nodemap.get and index.[has_node|rev|get_rev]
377
# hg <= 5.2 (02802fa87b74)
378
379
380
381
382
def getgetrev(cl):
    """Returns index.get_rev or nodemap.get (for pre-5.3 Mercurial)."""
    if util.safehasattr(cl.index, 'get_rev'):
        return cl.index.get_rev
    return cl.nodemap.get
383
384
385
386
387
388
389
390
391

@contextlib.contextmanager
def parentchange(repo):
    try:
        yield
    finally:
        # hg <= 5.2 (85c4cd73996b)
        if util.safehasattr(repo, '_quick_access_changeid_invalidate'):
            repo._quick_access_changeid_invalidate()
392

393
if util.safehasattr(mergemod, '_update'):
394
    def _update(*args, **kwargs):
395
396
397
        return mergemod._update(*args, **kwargs)
else:
    # hg <= 5.5 (2c86b9587740)
398
    def _update(*args, **kwargs):
399
        return mergemod.update(*args, **kwargs)
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415

if (util.safehasattr(mergemod, '_update')
    and util.safehasattr(mergemod, 'update')):

    def update(ctx):
        mergemod.update(ctx)

    def clean_update(ctx):
        mergemod.clean_update(ctx)
else:
    # hg <= 5.5 (c1b603cdc95a)
    def update(ctx):
        hg.updaterepo(ctx.repo(), ctx.node(), overwrite=False)

    def clean_update(ctx):
        hg.updaterepo(ctx.repo(), ctx.node(), overwrite=True)
416

417
def cleanupnodes(repo, replacements, operation, moves=None, metadata=None):
418
419
420
421
422
423
424
425
426
427
428
429
    # Use this condition as a proxy since the commit we care about
    # (b99903534e06) didn't change any signatures.
    if util.safehasattr(scmutil, 'nullrev'):
        fixedreplacements = replacements
    else:
        # hg <= 4.7 (b99903534e06)
        fixedreplacements = {}
        for oldnodes, newnodes in replacements.items():
            for oldnode in oldnodes:
                fixedreplacements[oldnode] = newnodes

    scmutil.cleanupnodes(repo, replacements=fixedreplacements, operation=operation,
430
                         moves=moves, metadata=metadata)
431
432
433
434
435
436
437
438
439
440
441

if util.safehasattr(cmdutil, 'format_changeset_summary'):
    def format_changeset_summary_fn(ui, repo, command, default_spec):
        def show(ctx):
            text = cmdutil.format_changeset_summary(ui, ctx, command=command,
                                                    default_spec=default_spec)
            ui.write(b'%s\n' % text)
        return show
else:
    # hg <= 5.6 (96fcc37a9c80)
    def format_changeset_summary_fn(ui, repo, command, default_spec):
442
443
        return logcmdutil.changesetdisplayer(ui, repo,
                                             {b'template': default_spec}).show