This instance will be upgraded to Heptapod 0.21.0rc2 on 2021-04-16 at 14:00 UTC+2 (a few minutes of down time)

scmutil.py 76.6 KB
Newer Older
1 2 3 4 5 6 7
# scmutil.py - Mercurial core utility functions
#
#  Copyright Matt Mackall <mpm@selenic.com>
#
# This software may be used and distributed according to the terms of the
# GNU General Public License version 2 or any later version.

Gregory Szorc's avatar
Gregory Szorc committed
8 9
from __future__ import absolute_import

10
import difflib
Gregory Szorc's avatar
Gregory Szorc committed
11 12 13
import errno
import glob
import os
14
import posixpath
Gregory Szorc's avatar
Gregory Szorc committed
15
import re
16
import subprocess
17
import weakref
Gregory Szorc's avatar
Gregory Szorc committed
18 19

from .i18n import _
20
from .node import (
21
    bin,
22 23
    hex,
    nullid,
24
    nullrev,
25
    short,
26 27 28
    wdirid,
    wdirrev,
)
29
from .pycompat import getattr
30
from .thirdparty import attr
Gregory Szorc's avatar
Gregory Szorc committed
31
from . import (
32
    copies as copiesmod,
Gregory Szorc's avatar
Gregory Szorc committed
33 34 35
    encoding,
    error,
    match as matchmod,
36
    obsolete,
37
    obsutil,
Gregory Szorc's avatar
Gregory Szorc committed
38 39
    pathutil,
    phases,
40
    policy,
41
    pycompat,
42
    requirements as requirementsmod,
43
    revsetlang,
Gregory Szorc's avatar
Gregory Szorc committed
44
    similar,
45
    smartset,
46
    url,
Gregory Szorc's avatar
Gregory Szorc committed
47
    util,
48
    vfs,
Gregory Szorc's avatar
Gregory Szorc committed
49
)
50

51
from .utils import (
52
    hashutil,
53
    procutil,
54 55 56
    stringutil,
)

Jun Wu's avatar
Jun Wu committed
57
if pycompat.iswindows:
Gregory Szorc's avatar
Gregory Szorc committed
58
    from . import scmwindows as scmplatform
59
else:
Gregory Szorc's avatar
Gregory Szorc committed
60
    from . import scmposix as scmplatform
61

62
parsers = policy.importmod('parsers')
63
rustrevlog = policy.importrust('revlog')
64

65
termsize = scmplatform.termsize
66

67

68 69 70
@attr.s(slots=True, repr=False)
class status(object):
    '''Struct with a list of files per status.
71

72 73 74
    The 'deleted', 'unknown' and 'ignored' properties are only
    relevant to the working copy.
    '''
75

76 77 78 79 80 81 82
    modified = attr.ib(default=attr.Factory(list))
    added = attr.ib(default=attr.Factory(list))
    removed = attr.ib(default=attr.Factory(list))
    deleted = attr.ib(default=attr.Factory(list))
    unknown = attr.ib(default=attr.Factory(list))
    ignored = attr.ib(default=attr.Factory(list))
    clean = attr.ib(default=attr.Factory(list))
83 84 85 86 87 88 89 90 91 92 93

    def __iter__(self):
        yield self.modified
        yield self.added
        yield self.removed
        yield self.deleted
        yield self.unknown
        yield self.ignored
        yield self.clean

    def __repr__(self):
94 95 96 97 98
        return (
            r'<status modified=%s, added=%s, removed=%s, deleted=%s, '
            r'unknown=%s, ignored=%s, clean=%s>'
        ) % tuple(pycompat.sysstr(stringutil.pprint(v)) for v in self)

99

100 101 102 103 104 105 106
def itersubrepos(ctx1, ctx2):
    """find subrepos in ctx1 or ctx2"""
    # Create a (subpath, ctx) mapping where we prefer subpaths from
    # ctx1. The subpaths from ctx2 are important when the .hgsub file
    # has been modified (in ctx2) but not yet committed (in ctx1).
    subpaths = dict.fromkeys(ctx2.substate, ctx2)
    subpaths.update(dict.fromkeys(ctx1.substate, ctx1))
107 108 109 110 111 112 113 114

    missing = set()

    for subpath in ctx2.substate:
        if subpath not in ctx1.substate:
            del subpaths[subpath]
            missing.add(subpath)

115
    for subpath, ctx in sorted(pycompat.iteritems(subpaths)):
116 117
        yield subpath, ctx.sub(subpath)

118 119 120 121 122 123 124
    # Yield an empty subrepo based on ctx1 for anything only in ctx2.  That way,
    # status and diff will have an accurate result when it does
    # 'sub.{status|diff}(rev2)'.  Otherwise, the ctx2 subrepo is compared
    # against itself.
    for subpath in missing:
        yield subpath, ctx2.nullsub(subpath, ctx1)

125

126 127 128 129 130 131 132 133 134 135 136
def nochangesfound(ui, repo, excluded=None):
    '''Report no changes for push/pull, excluded is None or a list of
    nodes excluded from the push/pull.
    '''
    secretlist = []
    if excluded:
        for n in excluded:
            ctx = repo[n]
            if ctx.phase() >= phases.secret and not ctx.extinct():
                secretlist.append(n)

137
    if secretlist:
138
        ui.status(
139
            _(b"no changes found (ignored %d secret changesets)\n")
140 141
            % len(secretlist)
        )
142
    else:
143
        ui.status(_(b"no changes found\n"))
144

145

146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177
def getsimilar(symbols, value):
    sim = lambda x: difflib.SequenceMatcher(None, value, x).ratio()
    # The cutoff for similarity here is pretty arbitrary. It should
    # probably be investigated and tweaked.
    return [s for s in symbols if sim(s) > 0.6]


def reportsimilar(write, similar):
    if len(similar) == 1:
        write(_(b"(did you mean %s?)\n") % similar[0])
    elif similar:
        ss = b", ".join(sorted(similar))
        write(_(b"(did you mean one of %s?)\n") % ss)


def formatparse(write, inst):
    similar = []
    if isinstance(inst, error.UnknownIdentifier):
        # make sure to check fileset first, as revset can invoke fileset
        similar = getsimilar(inst.symbols, inst.function)
    if inst.location is not None:
        write(
            _(b"hg: parse error at %s: %s\n")
            % (pycompat.bytestr(inst.location), inst.message)
        )
    else:
        write(_(b"hg: parse error: %s\n") % inst.message)
        reportsimilar(write, similar)
    if inst.hint:
        write(_(b"(%s)\n") % inst.hint)


178 179 180 181 182 183
def callcatch(ui, func):
    """call func() with global exception handling

    return func() if no exception happens. otherwise do some error handling
    and return an exit code accordingly. does not handle all exceptions.
    """
184 185
    coarse_exit_code = -1
    detailed_exit_code = -1
186
    try:
187 188
        try:
            return func()
189
        except:  # re-raises
190 191
            ui.traceback()
            raise
192 193 194
    # Global exception handling, alphabetically
    # Mercurial-specific first, followed by built-in and library exceptions
    except error.LockHeld as inst:
195
        detailed_exit_code = 20
196
        if inst.errno == errno.ETIMEDOUT:
197
            reason = _(b'timed out waiting for lock held by %r') % (
198 199
                pycompat.bytestr(inst.locker)
            )
200
        else:
201
            reason = _(b'lock held by %r') % inst.locker
202
        ui.error(
203
            _(b"abort: %s: %s\n")
204 205
            % (inst.desc or stringutil.forcebytestr(inst.filename), reason)
        )
206
        if not inst.locker:
207
            ui.error(_(b"(lock might be very busy)\n"))
208
    except error.LockUnavailable as inst:
209
        detailed_exit_code = 20
210
        ui.error(
211
            _(b"abort: could not lock %s: %s\n")
212 213 214 215 216
            % (
                inst.desc or stringutil.forcebytestr(inst.filename),
                encoding.strtolocal(inst.strerror),
            )
        )
217
    except error.OutOfBandError as inst:
218
        detailed_exit_code = 100
219
        if inst.args:
220
            msg = _(b"abort: remote error:\n")
221
        else:
222
            msg = _(b"abort: remote error\n")
223
        ui.error(msg)
224
        if inst.args:
225
            ui.error(b''.join(inst.args))
226
        if inst.hint:
227
            ui.error(b'(%s)\n' % inst.hint)
228
    except error.RepoError as inst:
229
        ui.error(_(b"abort: %s!\n") % inst)
230
        if inst.hint:
231
            ui.error(_(b"(%s)\n") % inst.hint)
232
    except error.ResponseError as inst:
233
        ui.error(_(b"abort: %s") % inst.args[0])
234 235 236
        msg = inst.args[1]
        if isinstance(msg, type(u'')):
            msg = pycompat.sysbytes(msg)
237
        if not isinstance(msg, bytes):
238
            ui.error(b" %r\n" % (msg,))
239
        elif not msg:
240
            ui.error(_(b" empty string\n"))
241
        else:
242
            ui.error(b"\n%r\n" % pycompat.bytestr(stringutil.ellipsis(msg)))
243
    except error.CensoredNodeError as inst:
244
        ui.error(_(b"abort: file censored %s!\n") % inst)
245
    except error.StorageError as inst:
246
        ui.error(_(b"abort: %s!\n") % inst)
247
        if inst.hint:
248
            ui.error(_(b"(%s)\n") % inst.hint)
249
    except error.InterventionRequired as inst:
250
        ui.error(b"%s\n" % inst)
251
        if inst.hint:
252
            ui.error(_(b"(%s)\n") % inst.hint)
253 254
        detailed_exit_code = 240
        coarse_exit_code = 1
255
    except error.WdirUnsupported:
256
        ui.error(_(b"abort: working directory revision cannot be specified\n"))
257
    except error.Abort as inst:
258 259
        if isinstance(inst, error.InputError):
            detailed_exit_code = 10
260 261
        elif isinstance(inst, error.StateError):
            detailed_exit_code = 20
262 263
        elif isinstance(inst, error.ConfigError):
            detailed_exit_code = 30
264 265
        elif isinstance(inst, error.CanceledError):
            detailed_exit_code = 250
266
        ui.error(_(b"abort: %s\n") % inst.message)
267
        if inst.hint:
268
            ui.error(_(b"(%s)\n") % inst.hint)
269 270 271
    except error.WorkerError as inst:
        # Don't print a message -- the worker already should have
        return inst.status_code
272
    except ImportError as inst:
273
        ui.error(_(b"abort: %s!\n") % stringutil.forcebytestr(inst))
274
        m = stringutil.forcebytestr(inst).split()[-1]
275 276 277 278
        if m in b"mpatch bdiff".split():
            ui.error(_(b"(did you forget to compile extensions?)\n"))
        elif m in b"zlib".split():
            ui.error(_(b"(is your Python install correct?)\n"))
279
    except util.urlerr.httperror as inst:
280
        detailed_exit_code = 100
281 282
        ui.error(_(b"abort: %s\n") % stringutil.forcebytestr(inst))
    except util.urlerr.urlerror as inst:
283
        detailed_exit_code = 100
284 285 286 287 288 289 290 291 292
        try:  # usually it is in the form (errno, strerror)
            reason = inst.reason.args[1]
        except (AttributeError, IndexError):
            # it might be anything, for example a string
            reason = inst.reason
        if isinstance(reason, pycompat.unicode):
            # SSLError of Python 2.7.9 contains a unicode
            reason = encoding.unitolocal(reason)
        ui.error(_(b"abort: error: %s\n") % stringutil.forcebytestr(reason))
293
    except (IOError, OSError) as inst:
294
        if (
295
            util.safehasattr(inst, b"args")
296 297 298
            and inst.args
            and inst.args[0] == errno.EPIPE
        ):
299
            pass
300
        elif getattr(inst, "strerror", None):  # common IOError or OSError
301
            if getattr(inst, "filename", None) is not None:
302
                ui.error(
303
                    _(b"abort: %s: '%s'\n")
304 305 306 307 308
                    % (
                        encoding.strtolocal(inst.strerror),
                        stringutil.forcebytestr(inst.filename),
                    )
                )
309
            else:
310
                ui.error(_(b"abort: %s\n") % encoding.strtolocal(inst.strerror))
311
        else:  # suspicious IOError
312 313
            raise
    except MemoryError:
314
        ui.error(_(b"abort: out of memory\n"))
315 316 317
    except SystemExit as inst:
        # Commands shouldn't sys.exit directly, but give a return code.
        # Just in case catch this and and pass exit code to caller.
318 319
        detailed_exit_code = 254
        coarse_exit_code = inst.code
320

321 322 323 324
    if ui.configbool(b'ui', b'detailed-exit-code'):
        return detailed_exit_code
    else:
        return coarse_exit_code
325

326

327
def checknewlabel(repo, lbl, kind):
328 329
    # Do not use the "kind" parameter in ui output.
    # It makes strings difficult to translate.
330
    if lbl in [b'tip', b'.', b'null']:
331
        raise error.InputError(_(b"the name '%s' is reserved") % lbl)
332
    for c in (b':', b'\0', b'\n', b'\r'):
333
        if c in lbl:
334
            raise error.InputError(
335
                _(b"%r cannot be used in a name") % pycompat.bytestr(c)
336
            )
337 338
    try:
        int(lbl)
339
        raise error.InputError(_(b"cannot use an integer as a name"))
340 341
    except ValueError:
        pass
342
    if lbl.strip() != lbl:
343 344 345
        raise error.InputError(
            _(b"leading or trailing whitespace in name %r") % lbl
        )
346

347

348 349
def checkfilename(f):
    '''Check that the filename f is an acceptable filename for a tracked file'''
350
    if b'\r' in f or b'\n' in f:
351
        raise error.InputError(
352
            _(b"'\\n' and '\\r' disallowed in filenames: %r")
353 354 355
            % pycompat.bytestr(f)
        )

356

357 358
def checkportable(ui, f):
    '''Check if filename f is portable and warn or abort depending on config'''
359
    checkfilename(f)
360 361
    abort, warn = checkportabilityalert(ui)
    if abort or warn:
362 363
        msg = util.checkwinfilename(f)
        if msg:
364
            msg = b"%s: %s" % (msg, procutil.shellquote(f))
365
            if abort:
366
                raise error.InputError(msg)
367
            ui.warn(_(b"warning: %s\n") % msg)
368

369

370 371 372
def checkportabilityalert(ui):
    '''check if the user's config requests nothing, a warning, or abort for
    non-portable filenames'''
373
    val = ui.config(b'ui', b'portablefilenames')
374
    lval = val.lower()
375
    bval = stringutil.parsebool(val)
376 377 378
    abort = pycompat.iswindows or lval == b'abort'
    warn = bval or lval == b'warn'
    if bval is None and not (warn or abort or lval == b'ignore'):
379
        raise error.ConfigError(
380
            _(b"ui.portablefilenames value is invalid ('%s')") % val
381
        )
382 383
    return abort, warn

384

385
class casecollisionauditor(object):
386
    def __init__(self, ui, abort, dirstate):
387 388
        self._ui = ui
        self._abort = abort
389 390
        allfiles = b'\0'.join(dirstate)
        self._loweredfiles = set(encoding.lower(allfiles).split(b'\0'))
391 392 393 394 395
        self._dirstate = dirstate
        # The purpose of _newfiles is so that we don't complain about
        # case collisions if someone were to call this object with the
        # same filename twice.
        self._newfiles = set()
396 397

    def __call__(self, f):
398 399
        if f in self._newfiles:
            return
400
        fl = encoding.lower(f)
401
        if fl in self._loweredfiles and f not in self._dirstate:
402
            msg = _(b'possible case-folding collision for %s') % f
403
            if self._abort:
404
                raise error.Abort(msg)
405
            self._ui.warn(_(b"warning: %s\n") % msg)
406 407
        self._loweredfiles.add(fl)
        self._newfiles.add(f)
408

409

410 411 412 413 414 415 416 417 418 419 420 421 422 423 424
def filteredhash(repo, maxrev):
    """build hash of filtered revisions in the current repoview.

    Multiple caches perform up-to-date validation by checking that the
    tiprev and tipnode stored in the cache file match the current repository.
    However, this is not sufficient for validating repoviews because the set
    of revisions in the view may change without the repository tiprev and
    tipnode changing.

    This function hashes all the revs filtered from the view and returns
    that SHA-1 digest.
    """
    cl = repo.changelog
    if not cl.filteredrevs:
        return None
425 426 427 428 429 430 431 432 433
    key = cl._filteredrevs_hashcache.get(maxrev)
    if not key:
        revs = sorted(r for r in cl.filteredrevs if r <= maxrev)
        if revs:
            s = hashutil.sha1()
            for rev in revs:
                s.update(b'%d;' % rev)
            key = s.digest()
            cl._filteredrevs_hashcache[maxrev] = key
434 435
    return key

436

437
def walkrepos(path, followsym=False, seen_dirs=None, recurse=False):
kiilerix's avatar
kiilerix committed
438 439
    '''yield every hg repository under path, always recursively.
    The recurse flag will only control recursion into repo working dirs'''
440

441 442 443
    def errhandler(err):
        if err.filename == path:
            raise err
444

445 446
    samestat = getattr(os.path, 'samestat', None)
    if followsym and samestat is not None:
447

448
        def adddir(dirlst, dirname):
449
            dirstat = os.stat(dirname)
450
            match = any(samestat(dirstat, lstdirstat) for lstdirstat in dirlst)
451 452 453
            if not match:
                dirlst.append(dirstat)
            return not match
454

455 456 457 458 459
    else:
        followsym = False

    if (seen_dirs is None) and followsym:
        seen_dirs = []
460
        adddir(seen_dirs, path)
461 462
    for root, dirs, files in os.walk(path, topdown=True, onerror=errhandler):
        dirs.sort()
463
        if b'.hg' in dirs:
464
            yield root  # found a repository
465 466
            qroot = os.path.join(root, b'.hg', b'patches')
            if os.path.isdir(os.path.join(qroot, b'.hg')):
467
                yield qroot  # we have a patch queue repo here
468 469
            if recurse:
                # avoid recursing inside the .hg directory
470
                dirs.remove(b'.hg')
471
            else:
472
                dirs[:] = []  # don't descend further
473 474 475 476
        elif followsym:
            newdirs = []
            for d in dirs:
                fname = os.path.join(root, d)
477
                if adddir(seen_dirs, fname):
478 479 480 481 482 483
                    if os.path.islink(fname):
                        for hgname in walkrepos(fname, True, seen_dirs):
                            yield hgname
                    else:
                        newdirs.append(d)
            dirs[:] = newdirs
484

485

486 487 488 489 490 491 492
def binnode(ctx):
    """Return binary node id for a given basectx"""
    node = ctx.node()
    if node is None:
        return wdirid
    return node

493

494 495
def intrev(ctx):
    """Return integer for a given basectx that can be used in comparison or
496
    arithmetic operation"""
497
    rev = ctx.rev()
498
    if rev is None:
499
        return wdirrev
500 501
    return rev

502

503 504
def formatchangeid(ctx):
    """Format changectx as '{rev}:{node|formatnode}', which is the default
505
    template provided by logcmdutil.changesettemplater"""
506 507 508
    repo = ctx.repo()
    return formatrevnode(repo.ui, intrev(ctx), binnode(ctx))

509

510 511 512 513 514 515
def formatrevnode(ui, rev, node):
    """Format given revision and node depending on the current verbosity"""
    if ui.debugflag:
        hexfunc = hex
    else:
        hexfunc = short
516
    return b'%d:%s' % (rev, hexfunc(node))
517

518

519
def resolvehexnodeidprefix(repo, prefix):
520
    if prefix.startswith(b'x'):
521
        prefix = prefix[1:]
522 523 524 525 526
    try:
        # Uses unfiltered repo because it's faster when prefix is ambiguous/
        # This matches the shortesthexnodeidprefix() function below.
        node = repo.unfiltered().changelog._partialmatch(prefix)
    except error.AmbiguousPrefixLookupError:
527 528 529
        revset = repo.ui.config(
            b'experimental', b'revisions.disambiguatewithin'
        )
530 531
        if revset:
            # Clear config to avoid infinite recursion
532
            configoverrides = {
533
                (b'experimental', b'revisions.disambiguatewithin'): None
534
            }
535 536 537 538 539 540 541 542 543 544
            with repo.ui.configoverride(configoverrides):
                revs = repo.anyrevs([revset], user=True)
                matches = []
                for rev in revs:
                    node = repo.changelog.node(rev)
                    if hex(node).startswith(prefix):
                        matches.append(node)
                if len(matches) == 1:
                    return matches[0]
        raise
545 546 547 548 549
    if node is None:
        return
    repo.changelog.rev(node)  # make sure node isn't filtered
    return node

550

551 552 553 554 555 556
def mayberevnum(repo, prefix):
    """Checks if the given prefix may be mistaken for a revision number"""
    try:
        i = int(prefix)
        # if we are a pure int, then starting with zero will not be
        # confused as a rev; or, obviously, if the int is larger
557 558 559
        # than the value of the tip rev. We still need to disambiguate if
        # prefix == '0', since that *is* a valid revnum.
        if (prefix != b'0' and prefix[0:1] == b'0') or i >= len(repo):
560 561 562 563 564
            return False
        return True
    except ValueError:
        return False

565

566 567 568 569 570 571
def shortesthexnodeidprefix(repo, node, minlength=1, cache=None):
    """Find the shortest unambiguous prefix that matches hexnode.

    If "cache" is not None, it must be a dictionary that can be used for
    caching between calls to this method.
    """
572 573 574
    # _partialmatch() of filtered changelog could take O(len(repo)) time,
    # which would be unacceptably slow. so we look for hash collision in
    # unfiltered space, which means some hashes may be slightly longer.
575

576
    minlength = max(minlength, 1)
577

578 579
    def disambiguate(prefix):
        """Disambiguate against revnums."""
580
        if repo.ui.configbool(b'experimental', b'revisions.prefixhexnode'):
581
            if mayberevnum(repo, prefix):
582
                return b'x' + prefix
583 584 585
            else:
                return prefix

586
        hexnode = hex(node)
587
        for length in range(len(prefix), len(hexnode) + 1):
588
            prefix = hexnode[:length]
589
            if not mayberevnum(repo, prefix):
590 591
                return prefix

592
    cl = repo.unfiltered().changelog
593
    revset = repo.ui.config(b'experimental', b'revisions.disambiguatewithin')
594
    if revset:
595 596
        revs = None
        if cache is not None:
597
            revs = cache.get(b'disambiguationrevset')
598 599 600
        if revs is None:
            revs = repo.anyrevs([revset], user=True)
            if cache is not None:
601
                cache[b'disambiguationrevset'] = revs
602 603
        if cl.rev(node) in revs:
            hexnode = hex(node)
604 605
            nodetree = None
            if cache is not None:
606
                nodetree = cache.get(b'disambiguationnodetree')
607
            if not nodetree:
608 609 610
                if util.safehasattr(parsers, 'nodetree'):
                    # The CExt is the only implementation to provide a nodetree
                    # class so far.
611 612 613 614 615
                    index = cl.index
                    if util.safehasattr(index, 'get_cindex'):
                        # the rust wrapped need to give access to its internal index
                        index = index.get_cindex()
                    nodetree = parsers.nodetree(index, len(revs))
616 617 618
                    for r in revs:
                        nodetree.insert(r)
                    if cache is not None:
619
                        cache[b'disambiguationnodetree'] = nodetree
620 621 622 623
            if nodetree is not None:
                length = max(nodetree.shortest(node), minlength)
                prefix = hexnode[:length]
                return disambiguate(prefix)
624 625 626 627 628 629 630 631 632 633
            for length in range(minlength, len(hexnode) + 1):
                matches = []
                prefix = hexnode[:length]
                for rev in revs:
                    otherhexnode = repo[rev].hex()
                    if prefix == otherhexnode[:length]:
                        matches.append(otherhexnode)
                if len(matches) == 1:
                    return disambiguate(prefix)

634
    try:
635
        return disambiguate(cl.shortest(node, minlength))
636 637
    except error.LookupError:
        raise error.RepoLookupError()
638

639

640
def isrevsymbol(repo, symbol):
641 642
    """Checks if a symbol exists in the repo.

643 644
    See revsymbol() for details. Raises error.AmbiguousPrefixLookupError if the
    symbol is an ambiguous nodeid prefix.
645
    """
646 647 648 649 650 651
    try:
        revsymbol(repo, symbol)
        return True
    except error.RepoLookupError:
        return False

652

653 654 655 656 657 658 659 660
def revsymbol(repo, symbol):
    """Returns a context given a single revision symbol (as string).

    This is similar to revsingle(), but accepts only a single revision symbol,
    i.e. things like ".", "tip", "1234", "deadbeef", "my-bookmark" work, but
    not "max(public())".
    """
    if not isinstance(symbol, bytes):
661
        msg = (
662 663
            b"symbol (%s of type %s) was not a string, did you mean "
            b"repo[symbol]?" % (symbol, type(symbol))
664
        )
665
        raise error.ProgrammingError(msg)
666
    try:
667
        if symbol in (b'.', b'tip', b'null'):
668 669 670 671
            return repo[symbol]

        try:
            r = int(symbol)
672
            if b'%d' % r != symbol:
673 674 675 676 677 678 679 680 681 682 683 684
                raise ValueError
            l = len(repo.changelog)
            if r < 0:
                r += l
            if r < 0 or r >= l and r != wdirrev:
                raise ValueError
            return repo[r]
        except error.FilteredIndexError:
            raise
        except (ValueError, OverflowError, IndexError):
            pass

685 686 687 688 689 690 691 692 693 694
        if len(symbol) == 40:
            try:
                node = bin(symbol)
                rev = repo.changelog.rev(node)
                return repo[rev]
            except error.FilteredLookupError:
                raise
            except (TypeError, LookupError):
                pass

695 696 697 698 699 700 701 702
        # look up bookmarks through the name interface
        try:
            node = repo.names.singlenode(repo, symbol)
            rev = repo.changelog.rev(node)
            return repo[rev]
        except KeyError:
            pass

703
        node = resolvehexnodeidprefix(repo, symbol)
704 705 706 707
        if node is not None:
            rev = repo.changelog.rev(node)
            return repo[rev]

708
        raise error.RepoLookupError(_(b"unknown revision '%s'") % symbol)
709

710 711
    except error.WdirUnsupported:
        return repo[None]
712 713 714 715 716
    except (
        error.FilteredIndexError,
        error.FilteredLookupError,
        error.FilteredRepoLookupError,
    ):
717 718
        raise _filterederror(repo, symbol)

719

720 721 722 723 724
def _filterederror(repo, changeid):
    """build an exception to be raised about a filtered changeid

    This is extracted in a function to help extensions (eg: evolve) to
    experiment with various message variants."""
725
    if repo.filtername.startswith(b'visible'):
726 727 728 729 730 731 732 733 734 735

        # Check if the changeset is obsolete
        unfilteredrepo = repo.unfiltered()
        ctx = revsymbol(unfilteredrepo, changeid)

        # If the changeset is obsolete, enrich the message with the reason
        # that made this changeset not visible
        if ctx.obsolete():
            msg = obsutil._getfilteredreason(repo, changeid, ctx)
        else:
736
            msg = _(b"hidden revision '%s'") % changeid
737

738
        hint = _(b'use --hidden to access hidden revisions')
739 740

        return error.FilteredRepoLookupError(msg, hint=hint)
741
    msg = _(b"filtered revision '%s' (not in '%s' subset)")
742 743
    msg %= (changeid, repo.filtername)
    return error.FilteredRepoLookupError(msg)
744

745

746
def revsingle(repo, revspec, default=b'.', localalias=None):
747
    if not revspec and revspec != 0:
748 749
        return repo[default]

750
    l = revrange(repo, [revspec], localalias=localalias)
751
    if not l:
752
        raise error.Abort(_(b'empty revision set'))
753
    return repo[l.last()]
754

755

756
def _pairspec(revspec):
757
    tree = revsetlang.parse(revspec)
758 759 760 761 762 763
    return tree and tree[0] in (
        b'range',
        b'rangepre',
        b'rangepost',
        b'rangeall',
    )
764

765

766 767
def revpair(repo, revs):
    if not revs:
768
        return repo[b'.'], repo[None]
769 770 771

    l = revrange(repo, revs)

772
    if not l:
773
        raise error.Abort(_(b'empty revision range'))
774

775 776
    first = l.first()
    second = l.last()
777

778 779 780 781 782
    if (
        first == second
        and len(revs) >= 2
        and not all(revrange(repo, [r]) for r in revs)
    ):
783
        raise error.Abort(_(b'empty revision on one side of range'))
784

785 786
    # if top-level is range expression, the result must always be a pair
    if first == second and len(revs) == 1 and not _pairspec(revs[0]):
Martin von Zweigbergk's avatar