BaseTools: add script to configure local git options
authorLeif Lindholm <leif.lindholm@linaro.org>
Mon, 10 Jun 2019 11:54:10 +0000 (19:54 +0800)
committerFeng, Bob C <bob.c.feng@intel.com>
Wed, 12 Jun 2019 02:10:11 +0000 (10:10 +0800)
Patch contribution and review is greatly simplified by following the
steps described in "Laszlo's unkempt guide":
https://github.com/tianocore/tianocore.github.io/wiki/Laszlo's-unkempt-git-guide-for-edk2-contributors-and-maintainers
but there are a lot of tedious manual steps in there, so here is a
python script that configures all options I am aware of
*for the repository the script is executed from*.

Signed-off-by: Leif Lindholm <leif.lindholm@linaro.org>
Acked-by: Laszlo Ersek <lersek@redhat.com>
Reviewed-by: Bob Feng <bob.c.feng@intel.com>
BaseTools/Scripts/SetupGit.py [new file with mode: 0644]

diff --git a/BaseTools/Scripts/SetupGit.py b/BaseTools/Scripts/SetupGit.py
new file mode 100644 (file)
index 0000000..3d39d3b
--- /dev/null
@@ -0,0 +1,204 @@
+## @file\r
+#  Set up the git configuration for contributing to TianoCore projects\r
+#\r
+#  Copyright (c) 2019, Linaro Ltd. All rights reserved.<BR>\r
+#\r
+#  SPDX-License-Identifier: BSD-2-Clause-Patent\r
+#\r
+\r
+from __future__ import print_function\r
+import argparse\r
+import os.path\r
+import re\r
+import sys\r
+\r
+try:\r
+    import git\r
+except ImportError:\r
+    print('Unable to load gitpython module - please install and try again.')\r
+    sys.exit(1)\r
+\r
+try:\r
+    # Try Python 2 'ConfigParser' module first since helpful lib2to3 will\r
+    # otherwise automagically load it with the name 'configparser'\r
+    import ConfigParser\r
+except ImportError:\r
+    # Otherwise, try loading the Python 3 'configparser' under an alias\r
+    try:\r
+        import configparser as ConfigParser\r
+    except ImportError:\r
+        print("Unable to load configparser/ConfigParser module - please install and try again!")\r
+        sys.exit(1)\r
+\r
+\r
+# Assumptions: Script is in edk2/BaseTools/Scripts,\r
+#              templates in edk2/BaseTools/Conf\r
+CONFDIR = os.path.join(os.path.dirname(os.path.dirname(os.path.realpath(__file__))),\r
+                       'Conf')\r
+\r
+UPSTREAMS = [\r
+    {'name': 'edk2',\r
+     'repo': 'https://github.com/tianocore/edk2.git',\r
+     'list': 'devel@edk2.groups.io'},\r
+    {'name': 'edk2-platforms',\r
+     'repo': 'https://github.com/tianocore/edk2-platforms.git',\r
+     'list': 'devel@edk2.groups.io', 'prefix': 'edk2-platforms'},\r
+    {'name': 'edk2-non-osi',\r
+     'repo': 'https://github.com/tianocore/edk2-non-osi.git',\r
+     'list': 'devel@edk2.groups.io', 'prefix': 'edk2-non-osi'}\r
+    ]\r
+\r
+# The minimum version required for all of the below options to work\r
+MIN_GIT_VERSION = (1, 9, 0)\r
+\r
+# Set of options to be set identically for all repositories\r
+OPTIONS = [\r
+    {'section': 'am',          'option': 'keepcr',         'value': True},\r
+    {'section': 'am',          'option': 'signoff',        'value': True},\r
+    {'section': 'cherry-pick', 'option': 'signoff',        'value': True},\r
+    {'section': 'color',       'option': 'diff',           'value': True},\r
+    {'section': 'color',       'option': 'grep',           'value': 'auto'},\r
+    {'section': 'commit',      'option': 'signoff',        'value': True},\r
+    {'section': 'core',        'option': 'abbrev',         'value': 12},\r
+    {'section': 'core',        'option': 'attributesFile',\r
+     'value': os.path.join(CONFDIR, 'gitattributes').replace('\\', '/')},\r
+    {'section': 'core',        'option': 'whitespace',     'value': 'cr-at-eol'},\r
+    {'section': 'diff',        'option': 'algorithm',      'value': 'patience'},\r
+    {'section': 'diff',        'option': 'orderFile',\r
+     'value': os.path.join(CONFDIR, 'diff.order').replace('\\', '/')},\r
+    {'section': 'diff',        'option': 'renames',        'value': 'copies'},\r
+    {'section': 'diff',        'option': 'statGraphWidth', 'value': '20'},\r
+    {'section': 'diff "ini"',    'option': 'xfuncname',\r
+     'value': '^\\\\[[A-Za-z0-9_., ]+]'},\r
+    {'section': 'format',      'option': 'coverLetter',    'value': True},\r
+    {'section': 'format',      'option': 'numbered',       'value': True},\r
+    {'section': 'format',      'option': 'signoff',        'value': False},\r
+    {'section': 'notes',       'option': 'rewriteRef',     'value': 'refs/notes/commits'},\r
+    {'section': 'sendemail',   'option': 'chainreplyto',   'value': False},\r
+    {'section': 'sendemail',   'option': 'thread',         'value': True},\r
+    ]\r
+\r
+\r
+def locate_repo():\r
+    """Opens a Repo object for the current tree, searching upwards in the directory hierarchy."""\r
+    try:\r
+        repo = git.Repo(path='.', search_parent_directories=True)\r
+    except (git.InvalidGitRepositoryError, git.NoSuchPathError):\r
+        print("It doesn't look like we're inside a git repository - aborting.")\r
+        sys.exit(2)\r
+    return repo\r
+\r
+\r
+def fuzzy_match_repo_url(one, other):\r
+    """Compares two repository URLs, ignoring protocol and optional trailing '.git'."""\r
+    oneresult   = re.match(r'.*://(?P<oneresult>.*?)(\.git)*$', one)\r
+    otherresult = re.match(r'.*://(?P<otherresult>.*?)(\.git)*$', other)\r
+\r
+    if oneresult and otherresult:\r
+        onestring = oneresult.group('oneresult')\r
+        otherstring = otherresult.group('otherresult')\r
+        if onestring == otherstring:\r
+            return True\r
+\r
+    return False\r
+\r
+\r
+def get_upstream(url):\r
+    """Extracts the dict for the current repo origin."""\r
+    for upstream in UPSTREAMS:\r
+        if fuzzy_match_repo_url(upstream['repo'], url):\r
+            return upstream\r
+    print("Unknown upstream '%s' - aborting!" % url)\r
+    sys.exit(3)\r
+\r
+\r
+def check_versions():\r
+    """Checks versions of dependencies."""\r
+    version = git.cmd.Git().version_info\r
+\r
+    if version < MIN_GIT_VERSION:\r
+        print('Need git version %d.%d or later!' % (version[0], version[1]))\r
+        sys.exit(4)\r
+\r
+\r
+def write_config_value(repo, section, option, data):\r
+    """."""\r
+    with repo.config_writer(config_level='repository') as configwriter:\r
+        configwriter.set_value(section, option, data)\r
+\r
+\r
+if __name__ == '__main__':\r
+    check_versions()\r
+\r
+    PARSER = argparse.ArgumentParser(\r
+        description='Sets up a git repository according to TianoCore rules.')\r
+    PARSER.add_argument('-c', '--check',\r
+                        help='check current config only, printing what would be changed',\r
+                        action='store_true',\r
+                        required=False)\r
+    PARSER.add_argument('-f', '--force',\r
+                        help='overwrite existing settings conflicting with program defaults',\r
+                        action='store_true',\r
+                        required=False)\r
+    PARSER.add_argument('-v', '--verbose',\r
+                        help='enable more detailed output',\r
+                        action='store_true',\r
+                        required=False)\r
+    ARGS = PARSER.parse_args()\r
+\r
+    REPO = locate_repo()\r
+    if REPO.bare:\r
+        print('Bare repo - please check out an upstream one!')\r
+        sys.exit(6)\r
+\r
+    URL = REPO.remotes.origin.url\r
+\r
+    UPSTREAM = get_upstream(URL)\r
+    if not UPSTREAM:\r
+        print("Upstream '%s' unknown, aborting!" % URL)\r
+        sys.exit(7)\r
+\r
+    # Set a list email address if our upstream wants it\r
+    if 'list' in UPSTREAM:\r
+        OPTIONS.append({'section': 'sendemail', 'option': 'to',\r
+                        'value': UPSTREAM['list']})\r
+    # Append a subject prefix entry to OPTIONS if our upstream wants it\r
+    if 'prefix' in UPSTREAM:\r
+        OPTIONS.append({'section': 'format', 'option': 'subjectPrefix',\r
+                        'value': "PATCH " + UPSTREAM['prefix']})\r
+\r
+    CONFIG = REPO.config_reader(config_level='repository')\r
+\r
+    for entry in OPTIONS:\r
+        exists = False\r
+        try:\r
+            # Make sure to read boolean/int settings as real type rather than strings\r
+            if isinstance(entry['value'], bool):\r
+                value = CONFIG.getboolean(entry['section'], entry['option'])\r
+            elif isinstance(entry['value'], int):\r
+                value = CONFIG.getint(entry['section'], entry['option'])\r
+            else:\r
+                value = CONFIG.get(entry['section'], entry['option'])\r
+\r
+            exists = True\r
+        # Don't bail out from options not already being set\r
+        except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):\r
+            pass\r
+\r
+        if exists:\r
+            if value == entry['value']:\r
+                if ARGS.verbose:\r
+                    print("%s.%s already set (to '%s')" % (entry['section'],\r
+                                                           entry['option'], value))\r
+            else:\r
+                if ARGS.force:\r
+                    write_config_value(REPO, entry['section'], entry['option'], entry['value'])\r
+                else:\r
+                    print("Not overwriting existing %s.%s value:" % (entry['section'],\r
+                                                                     entry['option']))\r
+                    print("  '%s' != '%s'" % (value, entry['value']))\r
+                    print("  add '-f' to command line to force overwriting existing settings")\r
+        else:\r
+            print("%s.%s => '%s'" % (entry['section'], entry['option'], entry['value']))\r
+            if not ARGS.check:\r
+                write_config_value(REPO, entry['section'], entry['option'], entry['value'])\r