## @file
#  Set up the git configuration for contributing to TianoCore projects
#
#  Copyright (c) 2019, Linaro Ltd. All rights reserved.<BR>
#  Copyright (c) 2019, Intel Corporation. All rights reserved.<BR>
#
#  SPDX-License-Identifier: BSD-2-Clause-Patent
#

from __future__ import print_function
import argparse
import os.path
import re
import sys

try:
    import git
except ImportError:
    print('Unable to load gitpython module - please install and try again.')
    sys.exit(1)

try:
    # Try Python 2 'ConfigParser' module first since helpful lib2to3 will
    # otherwise automagically load it with the name 'configparser'
    import ConfigParser
except ImportError:
    # Otherwise, try loading the Python 3 'configparser' under an alias
    try:
        import configparser as ConfigParser
    except ImportError:
        print("Unable to load configparser/ConfigParser module - please install and try again!")
        sys.exit(1)


# Assumptions: Script is in edk2/BaseTools/Scripts,
#              templates in edk2/BaseTools/Conf
CONFDIR = os.path.join(os.path.dirname(os.path.dirname(os.path.realpath(__file__))),
                       'Conf')

UPSTREAMS = [
    {'name': 'edk2',
     'repo': 'https://github.com/tianocore/edk2.git',
     'list': 'devel@edk2.groups.io'},
    {'name': 'edk2-platforms',
     'repo': 'https://github.com/tianocore/edk2-platforms.git',
     'list': 'devel@edk2.groups.io', 'prefix': 'edk2-platforms'},
    {'name': 'edk2-non-osi',
     'repo': 'https://github.com/tianocore/edk2-non-osi.git',
     'list': 'devel@edk2.groups.io', 'prefix': 'edk2-non-osi'},
    {'name': 'edk2-test',
     'repo': 'https://github.com/tianocore/edk2-test.git',
     'list': 'devel@edk2.groups.io', 'prefix': 'edk2-test'}
    ]

# The minimum version required for all of the below options to work
MIN_GIT_VERSION = (1, 9, 0)

# Set of options to be set identically for all repositories
OPTIONS = [
    {'section': 'alias',       'option': 'fp',
     'value': 'format-patch -M --stat=1000 --stat-graph-width=20'},
    {'section': 'am',          'option': 'keepcr',            'value': True},
    {'section': 'am',          'option': 'signoff',           'value': True},
    {'section': 'cherry-pick', 'option': 'signoff',           'value': True},
    {'section': 'color',       'option': 'diff',              'value': True},
    {'section': 'color',       'option': 'grep',              'value': 'auto'},
    {'section': 'commit',      'option': 'signoff',           'value': True},
    {'section': 'core',        'option': 'abbrev',            'value': 12},
    {'section': 'core',        'option': 'attributesFile',
     'value': os.path.join(CONFDIR, 'gitattributes').replace('\\', '/')},
    {'section': 'core',        'option': 'whitespace',        'value': 'cr-at-eol'},
    {'section': 'diff',        'option': 'algorithm',         'value': 'patience'},
    {'section': 'diff',        'option': 'orderFile',
     'value': os.path.join(CONFDIR, 'diff.order').replace('\\', '/')},
    {'section': 'diff',        'option': 'renames',           'value': 'copies'},
    {'section': 'diff',        'option': 'statGraphWidth',    'value': '20'},
    {'section': 'diff "ini"',  'option': 'xfuncname',
     'value': '^\\\\[[A-Za-z0-9_., ]+]'},
    {'section': 'format',      'option': 'coverLetter',       'value': True},
    {'section': 'format',      'option': 'numbered',          'value': True},
    {'section': 'format',      'option': 'signoff',           'value': False},
    {'section': 'log',         'option': 'mailmap',           'value': True},
    {'section': 'notes',       'option': 'rewriteRef',        'value': 'refs/notes/commits'},
    {'section': 'sendemail',   'option': 'chainreplyto',      'value': False},
    {'section': 'sendemail',   'option': 'thread',            'value': True},
    {'section': 'sendemail',   'option': 'transferEncoding',  'value': '8bit'},
    ]


def locate_repo():
    """Opens a Repo object for the current tree, searching upwards in the directory hierarchy."""
    try:
        repo = git.Repo(path='.', search_parent_directories=True)
    except (git.InvalidGitRepositoryError, git.NoSuchPathError):
        print("It doesn't look like we're inside a git repository - aborting.")
        sys.exit(2)
    return repo


def fuzzy_match_repo_url(one, other):
    """Compares two repository URLs, ignoring protocol and optional trailing '.git'."""
    oneresult   = re.match(r'.*://(?P<oneresult>.*?)(\.git)*$', one)
    otherresult = re.match(r'.*://(?P<otherresult>.*?)(\.git)*$', other)

    if oneresult and otherresult:
        onestring = oneresult.group('oneresult')
        otherstring = otherresult.group('otherresult')
        if onestring == otherstring:
            return True

    return False


def get_upstream(url, name):
    """Extracts the dict for the current repo origin."""
    for upstream in UPSTREAMS:
        if (fuzzy_match_repo_url(upstream['repo'], url) or
                upstream['name'] == name):
            return upstream
    print("Unknown upstream '%s' - aborting!" % url)
    sys.exit(3)


def check_versions():
    """Checks versions of dependencies."""
    version = git.cmd.Git().version_info

    if version < MIN_GIT_VERSION:
        print('Need git version %d.%d or later!' % (version[0], version[1]))
        sys.exit(4)


def write_config_value(repo, section, option, data):
    """."""
    with repo.config_writer(config_level='repository') as configwriter:
        configwriter.set_value(section, option, data)


if __name__ == '__main__':
    check_versions()

    PARSER = argparse.ArgumentParser(
        description='Sets up a git repository according to TianoCore rules.')
    PARSER.add_argument('-c', '--check',
                        help='check current config only, printing what would be changed',
                        action='store_true',
                        required=False)
    PARSER.add_argument('-f', '--force',
                        help='overwrite existing settings conflicting with program defaults',
                        action='store_true',
                        required=False)
    PARSER.add_argument('-n', '--name', type=str, metavar='repo',
                        choices=['edk2', 'edk2-platforms', 'edk2-non-osi', 'edk2-test'],
                        help='set the repo name to configure for, if not '
                             'detected automatically',
                        required=False)
    PARSER.add_argument('-v', '--verbose',
                        help='enable more detailed output',
                        action='store_true',
                        required=False)
    ARGS = PARSER.parse_args()

    REPO = locate_repo()
    if REPO.bare:
        print('Bare repo - please check out an upstream one!')
        sys.exit(6)

    URL = REPO.remotes.origin.url

    UPSTREAM = get_upstream(URL, ARGS.name)
    if not UPSTREAM:
        print("Upstream '%s' unknown, aborting!" % URL)
        sys.exit(7)

    # Set a list email address if our upstream wants it
    if 'list' in UPSTREAM:
        OPTIONS.append({'section': 'sendemail', 'option': 'to',
                        'value': UPSTREAM['list']})
    # Append a subject prefix entry to OPTIONS if our upstream wants it
    if 'prefix' in UPSTREAM:
        OPTIONS.append({'section': 'format', 'option': 'subjectPrefix',
                        'value': "PATCH " + UPSTREAM['prefix']})

    CONFIG = REPO.config_reader(config_level='repository')

    for entry in OPTIONS:
        exists = False
        try:
            # Make sure to read boolean/int settings as real type rather than strings
            if isinstance(entry['value'], bool):
                value = CONFIG.getboolean(entry['section'], entry['option'])
            elif isinstance(entry['value'], int):
                value = CONFIG.getint(entry['section'], entry['option'])
            else:
                value = CONFIG.get(entry['section'], entry['option'])

            exists = True
        # Don't bail out from options not already being set
        except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
            pass

        if exists:
            if value == entry['value']:
                if ARGS.verbose:
                    print("%s.%s already set (to '%s')" % (entry['section'],
                                                           entry['option'], value))
            else:
                if ARGS.force:
                    write_config_value(REPO, entry['section'], entry['option'], entry['value'])
                else:
                    print("Not overwriting existing %s.%s value:" % (entry['section'],
                                                                     entry['option']))
                    print("  '%s' != '%s'" % (value, entry['value']))
                    print("  add '-f' to command line to force overwriting existing settings")
        else:
            print("%s.%s => '%s'" % (entry['section'], entry['option'], entry['value']))
            if not ARGS.check:
                write_config_value(REPO, entry['section'], entry['option'], entry['value'])
