#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# Copyright © 2015-2018 Collabora Ltd.
#
# SPDX-License-Identifier: MPL-2.0
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.

# deb-git-version-gen — Make up a version number for a package
#
# Assumptions:
# * dch, dpkg-dev, git, python3, python3-debian are available
# * The current working directory is the root of a git repository
# * The current branch has upstream sources and a debian/ directory
# * Debian packaging tags look like debian/1.2.3-4 (or vendor/1.2.3-4vendor5)
#
# and with --upstream or --auto-upstream:
# * Upstream tags look like v1.2.3 or 1.2.3

import argparse
import json
import logging
import os
import shlex
import subprocess
import sys
import time

try:
    import typing
except ImportError:     # pragma: no cover
    pass
else:
    typing  # silence "unused" warnings

# python3-debian or python-debian is available in Debian, Fedora, etc.
from debian.deb822 import Deb822
from debian.debian_support import Version


logger = logging.getLogger('deb-git-version-gen')


def check_output(command, *args, **kwargs):
    logger.debug('%s', command)
    return subprocess.check_output(command, *args, **kwargs)


def check_call(command, *args, **kwargs):   # pragma: no cover
    logger.debug('%s', command)
    return subprocess.check_call(command, *args, **kwargs)


class Failure(Exception):
    """An exception that does not normally provoke a traceback"""


class Versioner:
    def __init__(
            self,
            *,
            branch_marker=None,         # type: typing.Optional[str]
            build_suffix='',
            avoid_build_suffix='',
            date_based=False,
            dch=False,
            debug=False,
            git='git',                  # type: str
            packaging_tag=None,         # type: typing.Optional[str]
            release=True,               # type: typing.Optional[bool]
            timestamp=None,             # type: typing.Optional[int]
            upstream=False,             # type: typing.Optional[bool]
            vendor=None                 # type: typing.Optional[str]
    ):
        # type: (...) -> None
        self.avoid_build_suffix = ''
        self.branch_marker = branch_marker
        self.build_suffix = build_suffix
        self.date_based = date_based
        self.dch = dch
        self.debug = debug
        self.git = shlex.split(git)
        self.packaging_tag = packaging_tag
        self.release = release
        self.timestamp = timestamp
        self.upstream = upstream
        self.vendor = vendor

        if 'GBP_GIT_DIR' in os.environ:
            self.git = [
                'env',
                'GIT_DIR={}'.format(os.environ['GBP_GIT_DIR']),
            ] + self.git

        if self.build_suffix:
            if avoid_build_suffix:
                if (
                    Version('0' + self.build_suffix) >
                    Version('0' + avoid_build_suffix)
                ):
                    raise Failure(
                        '--avoid-build-suffix must compare later than '
                        '--build-suffix'
                    )

                self.avoid_build_suffix = avoid_build_suffix
            else:
                self.avoid_build_suffix = self.build_suffix

    def parse_description(self, description):
        # type: (str) -> typing.Tuple[str, Version, int, str]
        """Parse `git describe --long` output.

        Return the name of the tag we used as a reference point,
        the Version that it represents, the number of commits since
        that tag to get to where we are now, and the short commit hash
        of where we are now.
        """
        try:
            return self._parse_description(description)
        except (ValueError, Failure) as e:      # pragma: no cover
            raise Failure(
                'Unable to parse `git describe` output {!r}: {}'.format(
                    description, str(e)))

    def _parse_description(self, description):
        # type: (str) -> typing.Tuple[str, Version, int, str]

        logger.debug('parsing description: %s', description)

        # (VVV, [WW,] NN, gCCCCCCC)
        parts = description.split('-')
        if len(parts) < 3:      # pragma: no cover
            raise Failure('Number of dash-separated parts is less than 3')
        # VVV[-WW]
        tag = '-'.join(parts[:-2])

        if '/' in tag:
            # Chop off debian/ resulting in VVV[-WW]-NN-gCCCCCCC.
            # If the version is like debian/openarena-textures/0.8.5split-11,
            # cope with that too.
            tagged_version = tag.rsplit('/', 1)[1]
        elif tag.startswith('v'):
            tagged_version = tag[1:]
        else:
            tagged_version = tag

        # undo gbp version mangling: 1%1.2_beta3 -> 1:1.2~beta3
        tagged_version = tagged_version.replace('%', ':').replace('_', '~')
        # NN
        counter = parts[-2]
        # CCCCCCC
        commit = parts[-1].lstrip('g')

        return tag, Version(tagged_version), int(counter), commit

    def count_all_commits(self):
        # type: () -> typing.Tuple[int, str]
        """Fallback mode if we can't find a tag: count all the commits since
        the beginning of history.

        Return the number of commits that have ever happened, and the
        short commit hash of the current commit. This is analogous to
        the last two return values from parse_description(), if we
        imagine that the tag found by `git describe` was just before the
        first commit to this repository (which of course can't happen).
        """
        counter = int(
            check_output(
                '{} rev-list HEAD | wc -l'.format(
                    ' '.join(shlex.quote(word) for word in self.git),
                ),
                shell=True,
                universal_newlines=True).strip())
        commit = check_output(
            self.git + ['show', '--pretty=format:%h', '-s'],
            universal_newlines=True).strip()
        return counter, commit

    @staticmethod
    def format_tag(tag_format, mangled_version):
        if '%(version)s' in tag_format:
            return tag_format % {'version': mangled_version}
        elif '%s' in tag_format:
            return tag_format % (mangled_version,)
        elif '*' in tag_format:
            return tag_format.replace('*', mangled_version)
        else:
            raise Failure(
                'Cannot work out how to substitute version into %s'
                % tag_format)

    def main(self):
        changelog_version = Version(
            check_output([
                'dpkg-parsechangelog', '-SVersion', '-n1'
            ], universal_newlines=True).strip('\n')
        )
        is_native = (changelog_version.debian_revision is None)

        if os.path.exists('debian/source/format'):
            source_format = open('debian/source/format').read().strip()
        else:
            source_format = '1.0'

        if source_format not in ('3.0 (quilt)', '3.0 (native)', '1.0'):
            raise Failure(
                'debian/source/format unsupported: {}'.format(source_format))

        if source_format == '3.0 (native)' and not is_native:
            raise Failure(
                'debian/source/format is native but version number is not')
        elif source_format == '3.0 (quilt)' and is_native:
            raise Failure(
                'debian/source/format is not native but version number is')

        release = False

        if self.debug:
            diag_sink = sys.stderr
        else:   # pragma: no cover
            diag_sink = subprocess.DEVNULL

        if (self.upstream is None or self.upstream) and not is_native:
            logger.debug('We are the upstream for this non-native package')
            # vVVV[-WW]-NN-gCCCCCCC
            try:
                raw_description = check_output(self.git + [
                    'describe', '--long', '--tags',
                    '--match=[v0-9]*',
                ], universal_newlines=True, stderr=diag_sink).strip()
            except subprocess.CalledProcessError:
                tag = None
                tagged_version = None
                counter, commit = self.count_all_commits()
                reference_point = 'beginning of history'
                logger.debug(
                    '%s is %d commits since beginning of history',
                    commit, counter)
            else:
                tag, tagged_version, counter, commit = self.parse_description(
                    raw_description)
                reference_point = tag
                logger.debug(
                    '%s is %d commits since tag %s (version %s)',
                    commit, counter, tag, tagged_version)

            if self.upstream:
                logger.debug('Explicitly making an upstream release')
                is_upstream = True
            elif counter == 0:
                logger.debug(
                    'Exactly at an upstream tag, presumably trying to '
                    'release it')
                is_upstream = True
            elif tagged_version is None:
                logger.debug(
                    'There are no tags, so we will have to do an upstream '
                    'release before we can do a downstream release')
                is_upstream = True
            else:
                assert tag is not None
                logger.debug(
                    'We are after an upstream tag. Checking whether the '
                    'differences are packaging-only or upstream...')
                is_upstream = False

                diffstat = check_output(self.git + [
                    'diff', '--name-only', '--no-renames', '-z',
                    tag + '..HEAD',
                ])
                for line in diffstat.split(b'\0'):
                    if line and not line.startswith(b'debian/'):
                        logger.debug('Upstream change found')
                        is_upstream = True
                        break
                else:
                    logger.debug('Only packaging changes found')
        else:
            is_upstream = False
            logger.debug('This is a packaging revision or native package')

        if is_native or not is_upstream:
            logger.debug('Looking for most recent packaging tag')
            offset = 0
            while offset < 10:
                tagged_version = check_output([
                    'dpkg-parsechangelog', '-SVersion',
                    '-o{}'.format(offset), '-n1',
                ], universal_newlines=True).strip('\n')
                # ~ and : are necessary syntax elements in some dpkg
                # version numbers but are not valid in git tags;
                # convert them in the same way as gbp,
                # 1:1.2~beta3 -> 1%1.2_beta3
                mangled_version = tagged_version.replace(':', '%')
                mangled_version = mangled_version.replace('~', '_')

                if self.packaging_tag is not None:
                    matches = [
                        self.format_tag(self.packaging_tag, mangled_version),
                    ]
                else:
                    if self.vendor is not None:
                        matches = [self.vendor.lower() + '/' + mangled_version]
                    else:
                        matches = ['*/' + mangled_version]

                    if is_native:
                        matches.append(mangled_version)
                        matches.append('v' + mangled_version)

                        if '%' in mangled_version:
                            without_epoch = mangled_version.split('%', 1)[1]
                            matches.append(without_epoch)
                            matches.append('v' + without_epoch)

                for match in matches:
                    try:
                        raw_description = check_output(self.git + [
                            'describe',
                            '--long',
                            '--tags',
                            '--match=' + match,
                        ], universal_newlines=True, stderr=diag_sink).strip()
                    except subprocess.CalledProcessError:
                        continue
                    else:
                        break
                else:
                    logger.debug('Not found, trying previous release')
                    offset += 1
                    continue

                _, tagged_version, counter, commit = self.parse_description(
                    raw_description)
                reference_point = str(tagged_version)
                logger.debug(
                    '%s is %d commits since packaging tag for version %s',
                    commit, counter, tagged_version)
                break
            else:
                # No recent Debian version has a tag, fall back to counting
                # every commit we've ever done (as though there was a tag
                # just before the beginning of history)
                tagged_version = None
                counter, commit = self.count_all_commits()
                reference_point = 'beginning of history'
                logger.debug(
                    '%s is %d commits since beginning of history',
                    commit, counter)

        if tagged_version is None:
            logger.debug(
                'Nothing has been tagged yet: making sure version is '
                'slightly before latest changelog entry %s',
                changelog_version)
            # Nothing has been tagged yet, and the changelog says something
            # like "foo (vvv[-ww]) UNRELEASED; urgency=low". Choose a
            # version that is not only less than vvv[-ww], but also less
            # than what would appear if we did have a tag for version
            # VVV[-WW] < vvv available, by appending '~' (which sorts
            # before the empty string), twice (so it sorts before '~').
            snapshot_version = Version(changelog_version)

            if is_upstream or is_native:
                snapshot_version.upstream_version += '~~'
            else:
                snapshot_version.debian_version += '~~'
        else:
            # VVV[-WW]
            snapshot_version = Version(tagged_version)

        if snapshot_version.epoch is None:
            logger.debug(
                'Using epoch %s from changelog',
                changelog_version.epoch)
            snapshot_version.epoch = changelog_version.epoch

        release = self.release

        if release is None:
            logger.debug(
                'Automatically determining whether this is a release... %s',
                str(counter == 0))
            release = (counter == 0)

        if release and counter != 0:
            raise Failure(
                'Cannot release: {} is {} commits after {}'.format(
                    commit, counter, reference_point))

        if self.date_based:
            nowish = self.timestamp
            if nowish is None and 'SOURCE_DATE_EPOCH' in os.environ:
                nowish = int(os.getenv('SOURCE_DATE_EPOCH'))
            timestamp = time.strftime('%Y%m%d.%H%M%S', time.gmtime(nowish))
            snapshot_marker = '+{}+g{}'.format(timestamp, commit)
        else:
            snapshot_marker = '+{}+g{}'.format(counter, commit)

        if self.branch_marker:
            snapshot_marker = self.branch_marker + snapshot_marker

        if (
            self.build_suffix and
            Version('0' + self.build_suffix) > Version('0' + snapshot_marker)
        ):
            # Make sure the new snapshot will supersede the tagged
            # version even after the build suffix is appended.
            # For example, if we go from 1.2-3 to 1.2-3+6+...,
            # it wouldn't show as newer than 1.2-3+b1; but if
            # avoid_build_suffix is set to +b or to +snapshot,
            # then we can use 1.2-3+b+6+... or 1.2-3+snapshot+6+...
            # to bypass the problem.
            #
            # We only need to do this if the changelog has not already
            # been updated.
            reference = Version(changelog_version)

            if is_native or is_upstream:
                reference.debian_revision = None

            if reference == Version(tagged_version):
                snapshot_marker = self.avoid_build_suffix + snapshot_marker

        if counter == 0 and release and not self.branch_marker:
            # we are exactly at a tag and trying to release: use it
            if changelog_version.upstream_version == \
                    snapshot_version.upstream_version:
                # we are exactly at an upstream tag and the changelog
                # describes a corresponding Debian revision: go with that
                snapshot_version = changelog_version
        elif is_upstream or is_native:
            if is_native and snapshot_version.debian_revision is not None:
                # Rare special case: we are switching a non-native package
                # to be native, so we need to get rid of the Debian revision.
                # Replace VVV-WW with VVV+WW+gCCCCCCC
                snapshot_version.upstream_version = '{}+{}{}'.format(
                    snapshot_version.upstream_version,
                    snapshot_version.debian_revision,
                    snapshot_marker,
                )
                snapshot_version.debian_revision = None
            else:
                # VVV+NN+gCCCCCCC
                snapshot_version.upstream_version = '{}{}'.format(
                        snapshot_version.upstream_version, snapshot_marker)
                if not is_native:
                    # We need some sort of Debian revision number, and the one
                    # in the changelog is meaningless because we've bumped the
                    # upstream version, so use VVV+WW+gCCCCCCC-0~snapshot
                    snapshot_version.debian_revision = '0~snapshot'
        elif snapshot_version.debian_revision is None:
            # Rare special case: we are going from a native version
            # VVV to a non-native version VVV-WW. Use VVV-0+NN+gCCCCCCC,
            # as though the native package had been a non-native package
            # with Debian revision 0
            snapshot_version.debian_revision = '0{}'.format(snapshot_marker)
        else:
            # VVV-WW+NN+gCCCCCCC
            snapshot_version.debian_revision = '{}{}'.format(
                    snapshot_version.debian_revision, snapshot_marker)

        assert is_native or snapshot_version.debian_revision is not None
        assert not is_native or snapshot_version.debian_revision is None

        if (tagged_version is not None and
                changelog_version.upstream_version
                != Version(tagged_version).upstream_version):
            # git says the latest tag is foo_1.2 but debian/changelog says
            # we are foo_2.0-1, or something. This probably means someone
            # is preparing to make a release with new API/ABI.
            # Make the snapshot version "slightly less than" the version
            # being prepared, for example 2.0~1.2+3+g567890a instead of
            # 1.2+3+g567890a, so that it satisfies backport-friendly
            # dependencies like foo (>= 2.0~).
            # Conveniently, 2.0~1.2+... is less than 2.0~beta1, so
            # genuine prereleases will normally sort higher than snapshots.
            if is_native or is_upstream:
                snapshot_version.upstream_version = '{}~{}'.format(
                    changelog_version.upstream_version,
                    snapshot_version.upstream_version).replace(':', 'e')
            else:
                # We have to leave the upstream version as it is, otherwise
                # we'd have the wrong orig tarball; make the Debian revision
                # small instead, like 2.0-0~1.2+0vendor1+3+g567890a
                # (which we want to be less than 2.0-0vendor1)
                debian_revision = '{}~{}'.format(
                    changelog_version.debian_revision,
                    snapshot_version).replace('-', '+').replace(':', 'e')
                snapshot_version = Version(changelog_version)
                snapshot_version.debian_revision = debian_revision
        elif (tagged_version is not None and
                (changelog_version.upstream_version ==
                 Version(tagged_version).upstream_version) and
                changelog_version.debian_revision is not None and
                Version(tagged_version).debian_revision is not None and
                (changelog_version.debian_revision !=
                 Version(tagged_version).debian_revision)):
            # git says the latest tag is foo_1.2-3 but debian/changelog says
            # we are foo_1.2-4. Make sure we satisfy foo (>= 1.2-4~)
            snapshot_version.debian_revision = '{}~{}'.format(
                changelog_version.debian_revision,
                snapshot_version.debian_revision).replace('-', '+')
            snapshot_version.upstream_version = \
                changelog_version.upstream_version

        if '/' in str(snapshot_version):    # pragma: no cover
            raise Failure('Bad version {!r}'.format(snapshot_version))

        ret = dict(
            changelog_version=str(changelog_version),
            commit=commit,
            counter=counter,
            is_native=is_native,
            is_upstream=is_upstream,
            snapshot_version=str(snapshot_version),
        )

        if tagged_version is not None:
            ret['tagged_version'] = str(tagged_version)

        # If the version in the changelog is not already what we want,
        # make it so
        if changelog_version != snapshot_version:
            dch_argv = [
                'env', 'DEBFULLNAME=Snapshot',
                'DEBEMAIL=snapshot@localhost',
                'dch',
                '--no-conf',        # must be first, do not re-order
                '--newversion', str(snapshot_version),
                '--allow-lower-version', '.*',
                '--release-heuristic', 'changelog',
                'snapshot: commit {} ({} commits after {})'.format(
                    commit, counter, reference_point),
            ]

            if self.dch:    # pragma: no cover
                check_call(dch_argv)

                with open(
                    'debian/changelog'
                ) as reader, open(
                    'debian/changelog.dch', 'w'
                ) as writer:
                    writer.write(reader.readline())
                    writer.write(reader.readline())
                    writer.write('  * Snapshot build (local package)\n')
                    writer.write('\n')

                    for line in reader:
                        writer.write(line)

                    os.rename('debian/changelog.dch', 'debian/changelog')
            else:
                ret['snapshot_message'] = dch_argv[-1]
                ret['dch'] = ' '.join(shlex.quote(a) for a in dch_argv)

        return ret


class Config:
    """
    Configuration from debian/git-version-gen.control, which is a
    deb822 file like debian/control.

    Avoid-Build-Suffix: +snapshot
    Branch-Marker: +bug1234
    Build-Suffix: +b
    Packaging-Tag-Format: debian/*
    Snapshot-Marker: date|commit
    Upstream: yes|no|auto
    Vendor: debian
    """
    # Not configurable:
    # --debug: Doesn't make sense to commit to git
    # --debchange/--json: Are facts about the calling convention of
    #   the tool, not about the package
    # --release/--guess-release: Doesn't make sense to commit to git
    # --mock-git: Arbitrary code execution
    # --timestamp: Doesn't make sense to commit to git

    def __init__(self):
        self.avoid_build_suffix = ''
        self.build_suffix = ''
        self.branch_marker = None
        self.date_based = False
        self.packaging_tag = None
        self.upstream = False
        self.vendor = None

    def load(self):
        filename = os.path.join('debian', 'git-version-gen.control')

        if os.path.exists(filename):
            with open(filename) as reader:
                loader = Deb822(reader)

            for field in loader:
                if field.lower() not in (
                    'avoid-build-suffix',
                    'branch-marker',
                    'build-suffix',
                    'packaging-tag-format',
                    'snapshot-marker',
                    'upstream',
                ):
                    logger.warning(
                        '{}: Unknown field "{}"'.format(
                            filename,
                            field,
                        )
                    )

            if 'Upstream' in loader:
                if loader['Upstream'] == 'yes':
                    self.upstream = True
                elif loader['Upstream'] == 'auto':
                    self.upstream = None
                elif loader['Upstream'] == 'no':
                    self.upstream = False
                else:
                    raise Failure(
                        '{}: Upstream field must be yes|no|auto, '
                        'not {}'.format(
                            filename,
                            loader['Upstream'],
                        )
                    )

            if 'Snapshot-Marker' in loader:
                if loader['Snapshot-Marker'] == 'date':
                    self.date_based = True
                elif loader['Snapshot-Marker'] == 'commit':
                    self.date_based = False
                else:
                    raise Failure(
                        '{}: Snapshot-Marker field must be date|commit, '
                        'not {}'.format(
                            filename,
                            loader['Snapshot-Marker'],
                        )
                    )

            self.packaging_tag = loader.get(
                'Packaging-Tag-Format',
                self.packaging_tag,
            )

            self.avoid_build_suffix = loader.get(
                'Avoid-Build-Suffix',
                self.avoid_build_suffix,
            )

            self.build_suffix = loader.get(
                'Build-Suffix',
                self.build_suffix,
            )

            self.branch_marker = loader.get(
                'Branch-Marker',
                self.branch_marker,
            )

            self.vendor = loader.get('Vendor', self.vendor)


def main():
    # type: () -> None

    logging.getLogger().setLevel(logging.INFO)

    if sys.stderr.isatty():     # pragma: no cover
        try:
            import colorlog
        except ImportError:
            pass
        else:
            formatter = colorlog.ColoredFormatter(
                '%(log_color)s%(levelname)s:%(name)s:%(reset)s %(message)s')
            handler = logging.StreamHandler()
            handler.setFormatter(formatter)
            logging.getLogger().addHandler(handler)

    # This is a no-op if we already attached a (coloured log) handler
    logging.basicConfig()

    parser = argparse.ArgumentParser(
        description='Make up a version number for a package')

    try:
        config = Config()
        config.load()
    except Failure as e:
        logger.error('%s', e)
        sys.exit(1)

    parser.add_argument(
        '--packaging', '--packaging-only', '-p',
        help='assume that we are only packaging this package (default)',
        action='store_false', dest='upstream', default=config.upstream)
    parser.add_argument(
        '--upstream', '-u',
        help='assume that we are the upstream for this package',
        action='store_true', default=config.upstream)
    parser.add_argument(
        '--auto-upstream',
        help=(
            'assume that we are the upstream for this package, '
            'but do pure packaging changes as packaging revisions'
        ),
        action='store_const', const=None, dest='upstream')
    parser.add_argument(
        '--release', help='make a real release, not a snapshot',
        action='store_true', default=False)
    parser.add_argument(
        '--guess-release',
        help='make a real release if we are currently at a tag',
        action='store_const', const=None, dest='release')
    parser.add_argument(
        '--dch', '--debchange',
        help='run debchange to update debian/changelog',
        action='store_true', default=False)

    parser.add_argument(
        '--packaging-tag', '--debian-tag',
        help='Format for git tags corresponding to a packaging version',
        default=config.packaging_tag)
    parser.add_argument(
        '--vendor', '--distro', help='DEP-14 vendor/distribution string',
        default=config.vendor)

    parser.add_argument(
        '--counter-based',
        help='base version numbers on commit count (default)',
        action='store_false', dest='date_based',
        default=config.date_based)
    parser.add_argument(
        '--date-based', help='base version numbers on build date',
        action='store_true', default=config.date_based)
    parser.add_argument(
        '--timestamp',
        type=int,
        default=None,
        help=(
            'Version the package as though the time was this many seconds '
            'since the epoch (default: $SOURCE_DATE_EPOCH or now)'
        ),
    )

    parser.add_argument(
        '--branch-marker', help='add this string to snapshot marker',
        default=config.branch_marker)
    parser.add_argument(
        '--build-suffix',
        help=(
            'ensure snapshot marker is greater than this, e.g. "+b" for '
            'Debian binNMUs'
        ),
        default=config.build_suffix,
    )
    parser.add_argument(
        '--avoid-build-suffix',
        help=(
            'use this marker to avoid being less than --build-suffix, '
            'e.g. "+snapshot" (default: same as --build-suffix)'
        ),
        default=config.avoid_build_suffix)

    parser.add_argument(
        '--json',
        help='produce JSON output (default: just the snapshot version)',
        action='store_true')

    parser.add_argument(
        '--debug',
        help='Give more diagnostics',
        action='store_true')

    # For testing
    parser.add_argument(
        '--mock-git',
        dest='git',
        default='git',
        help=argparse.SUPPRESS,
    )

    args = parser.parse_args()

    if args.debug:
        logging.getLogger().setLevel(logging.DEBUG)

    try:
        version_info = Versioner(
            avoid_build_suffix=args.avoid_build_suffix,
            build_suffix=args.build_suffix,
            branch_marker=args.branch_marker,
            date_based=args.date_based,
            dch=args.dch,
            debug=args.debug,
            git=args.git,
            packaging_tag=args.packaging_tag,
            release=args.release,
            timestamp=args.timestamp,
            upstream=args.upstream,
            vendor=args.vendor
        ).main()
    except (Failure, subprocess.CalledProcessError) as e:
        if args.debug:
            raise
        else:   # pragma: no cover
            logger.error('%s', e)
            sys.exit(1)

    if args.json:
        json.dump(version_info, sys.stdout)
        print('')
    else:
        print(version_info['snapshot_version'])


if __name__ == '__main__':
    main()
