| #!/usr/bin/env python |
| # |
| # ======- git-llvm - LLVM Git Help Integration ---------*- python -*--========# |
| # |
| # Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. |
| # See https://llvm.org/LICENSE.txt for license information. |
| # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception |
| # |
| # ==------------------------------------------------------------------------==# |
| |
| """ |
| git-llvm integration |
| ==================== |
| |
| This file provides integration for git. |
| |
| The git llvm push sub-command can be used to push changes to GitHub. It is |
| designed to be a thin wrapper around git, and its main purpose is to |
| detect and prevent merge commits from being pushed to the main repository. |
| |
| Usage: |
| |
| git-llvm push <upstream-branch> |
| |
| This will push changes from the current HEAD to the branch <upstream-branch>. |
| |
| """ |
| |
| from __future__ import print_function |
| import argparse |
| import collections |
| import os |
| import re |
| import shutil |
| import subprocess |
| import sys |
| import time |
| import getpass |
| assert sys.version_info >= (2, 7) |
| |
| try: |
| dict.iteritems |
| except AttributeError: |
| # Python 3 |
| def iteritems(d): |
| return iter(d.items()) |
| else: |
| # Python 2 |
| def iteritems(d): |
| return d.iteritems() |
| |
| try: |
| # Python 3 |
| from shlex import quote |
| except ImportError: |
| # Python 2 |
| from pipes import quote |
| |
| # It's *almost* a straightforward mapping from the monorepo to svn... |
| LLVM_MONOREPO_SVN_MAPPING = { |
| d: (d + '/trunk') |
| for d in [ |
| 'clang-tools-extra', |
| 'compiler-rt', |
| 'debuginfo-tests', |
| 'dragonegg', |
| 'klee', |
| 'libc', |
| 'libclc', |
| 'libcxx', |
| 'libcxxabi', |
| 'libunwind', |
| 'lld', |
| 'lldb', |
| 'llgo', |
| 'llvm', |
| 'openmp', |
| 'parallel-libs', |
| 'polly', |
| 'pstl', |
| ] |
| } |
| LLVM_MONOREPO_SVN_MAPPING.update({'clang': 'cfe/trunk'}) |
| LLVM_MONOREPO_SVN_MAPPING.update({'': 'monorepo-root/trunk'}) |
| |
| SPLIT_REPO_NAMES = {'llvm-' + d: d + '/trunk' |
| for d in ['www', 'zorg', 'test-suite', 'lnt']} |
| |
| VERBOSE = False |
| QUIET = False |
| dev_null_fd = None |
| |
| GIT_ORG = 'llvm' |
| GIT_REPO = 'llvm-project' |
| GIT_URL = 'github.com/{}/{}.git'.format(GIT_ORG, GIT_REPO) |
| |
| |
| def eprint(*args, **kwargs): |
| print(*args, file=sys.stderr, **kwargs) |
| |
| |
| def log(*args, **kwargs): |
| if QUIET: |
| return |
| print(*args, **kwargs) |
| |
| |
| def log_verbose(*args, **kwargs): |
| if not VERBOSE: |
| return |
| print(*args, **kwargs) |
| |
| |
| def die(msg): |
| eprint(msg) |
| sys.exit(1) |
| |
| |
| def ask_confirm(prompt): |
| # Python 2/3 compatibility |
| try: |
| read_input = raw_input |
| except NameError: |
| read_input = input |
| |
| while True: |
| query = read_input('%s (y/N): ' % (prompt)) |
| if query.lower() not in ['y','n', '']: |
| print('Expect y or n!') |
| continue |
| return query.lower() == 'y' |
| |
| |
| def get_dev_null(): |
| """Lazily create a /dev/null fd for use in shell()""" |
| global dev_null_fd |
| if dev_null_fd is None: |
| dev_null_fd = open(os.devnull, 'w') |
| return dev_null_fd |
| |
| |
| def shell(cmd, strip=True, cwd=None, stdin=None, die_on_failure=True, |
| ignore_errors=False, text=True, print_raw_stderr=False): |
| # Escape args when logging for easy repro. |
| quoted_cmd = [quote(arg) for arg in cmd] |
| log_verbose('Running in %s: %s' % (cwd, ' '.join(quoted_cmd))) |
| |
| err_pipe = subprocess.PIPE |
| if ignore_errors: |
| # Silence errors if requested. |
| err_pipe = get_dev_null() |
| |
| start = time.time() |
| p = subprocess.Popen(cmd, cwd=cwd, stdout=subprocess.PIPE, stderr=err_pipe, |
| stdin=subprocess.PIPE, |
| universal_newlines=text) |
| stdout, stderr = p.communicate(input=stdin) |
| elapsed = time.time() - start |
| |
| log_verbose('Command took %0.1fs' % elapsed) |
| |
| if p.returncode == 0 or ignore_errors: |
| if stderr and not ignore_errors: |
| if not print_raw_stderr: |
| eprint('`%s` printed to stderr:' % ' '.join(quoted_cmd)) |
| eprint(stderr.rstrip()) |
| if strip: |
| if text: |
| stdout = stdout.rstrip('\r\n') |
| else: |
| stdout = stdout.rstrip(b'\r\n') |
| if VERBOSE: |
| for l in stdout.splitlines(): |
| log_verbose("STDOUT: %s" % l) |
| return stdout |
| err_msg = '`%s` returned %s' % (' '.join(quoted_cmd), p.returncode) |
| eprint(err_msg) |
| if stderr: |
| eprint(stderr.rstrip()) |
| if die_on_failure: |
| sys.exit(2) |
| raise RuntimeError(err_msg) |
| |
| |
| def git(*cmd, **kwargs): |
| return shell(['git'] + list(cmd), **kwargs) |
| |
| |
| def svn(cwd, *cmd, **kwargs): |
| return shell(['svn'] + list(cmd), cwd=cwd, **kwargs) |
| |
| |
| def program_exists(cmd): |
| if sys.platform == 'win32' and not cmd.endswith('.exe'): |
| cmd += '.exe' |
| for path in os.environ["PATH"].split(os.pathsep): |
| if os.access(os.path.join(path, cmd), os.X_OK): |
| return True |
| return False |
| |
| |
| def get_fetch_url(): |
| return 'https://{}'.format(GIT_URL) |
| |
| |
| def get_push_url(user='', ssh=False): |
| |
| if ssh: |
| return 'ssh://git@{}'.format(GIT_URL) |
| |
| return 'https://{}'.format(GIT_URL) |
| |
| |
| def get_revs_to_push(branch): |
| # Fetch the latest upstream to determine which commits will be pushed. |
| git('fetch', get_fetch_url(), branch) |
| |
| commits = git('rev-list', '--ancestry-path', 'FETCH_HEAD..HEAD').splitlines() |
| # Reverse the order so we commit the oldest commit first |
| commits.reverse() |
| return commits |
| |
| |
| def git_push_one_rev(rev, dry_run, branch, ssh): |
| # Check if this a merge commit by counting the number of parent commits. |
| # More than 1 parent commmit means this is a merge. |
| num_parents = len(git('show', '--no-patch', '--format="%P"', rev).split()) |
| |
| if num_parents > 1: |
| raise Exception("Merge commit detected, cannot push ", rev) |
| |
| if num_parents != 1: |
| raise Exception("Error detecting number of parents for ", rev) |
| |
| if dry_run: |
| print("[DryRun] Would push", rev) |
| return |
| |
| # Second push to actually push the commit |
| git('push', get_push_url(ssh=ssh), '{}:{}'.format(rev, branch), print_raw_stderr=True) |
| |
| |
| def cmd_push(args): |
| '''Push changes to git:''' |
| dry_run = args.dry_run |
| |
| revs = get_revs_to_push(args.branch) |
| |
| if not revs: |
| die('Nothing to push') |
| |
| log('%sPushing %d commit%s:\n%s' % |
| ('[DryRun] ' if dry_run else '', len(revs), |
| 's' if len(revs) != 1 else '', |
| '\n'.join(' ' + git('show', '--oneline', '--quiet', c) |
| for c in revs))) |
| |
| # Ask confirmation if multiple commits are about to be pushed |
| if not args.force and len(revs) > 1: |
| if not ask_confirm("Are you sure you want to create %d commits?" % len(revs)): |
| die("Aborting") |
| |
| for r in revs: |
| git_push_one_rev(r, dry_run, args.branch, args.ssh) |
| |
| |
| if __name__ == '__main__': |
| if not program_exists('git'): |
| die('error: git-llvm needs git command, but git is not installed.') |
| |
| argv = sys.argv[1:] |
| p = argparse.ArgumentParser( |
| prog='git llvm', formatter_class=argparse.RawDescriptionHelpFormatter, |
| description=__doc__) |
| subcommands = p.add_subparsers(title='subcommands', |
| description='valid subcommands', |
| help='additional help') |
| verbosity_group = p.add_mutually_exclusive_group() |
| verbosity_group.add_argument('-q', '--quiet', action='store_true', |
| help='print less information') |
| verbosity_group.add_argument('-v', '--verbose', action='store_true', |
| help='print more information') |
| |
| parser_push = subcommands.add_parser( |
| 'push', description=cmd_push.__doc__, |
| help='push changes back to the LLVM SVN repository') |
| parser_push.add_argument( |
| '-n', |
| '--dry-run', |
| dest='dry_run', |
| action='store_true', |
| help='Do everything other than commit to svn. Leaves junk in the svn ' |
| 'repo, so probably will not work well if you try to commit more ' |
| 'than one rev.') |
| parser_push.add_argument( |
| '-s', |
| '--ssh', |
| dest='ssh', |
| action='store_true', |
| help='Use the SSH protocol for authentication, ' |
| 'instead of HTTPS with username and password.') |
| parser_push.add_argument( |
| '-f', |
| '--force', |
| action='store_true', |
| help='Do not ask for confirmation when pushing multiple commits.') |
| parser_push.add_argument( |
| 'branch', |
| metavar='GIT_BRANCH', |
| type=str, |
| default='master', |
| nargs='?', |
| help="branch to push (default: everything not in the branch's " |
| 'upstream)') |
| parser_push.set_defaults(func=cmd_push) |
| |
| args = p.parse_args(argv) |
| VERBOSE = args.verbose |
| QUIET = args.quiet |
| |
| # Python3 workaround, for when not arguments are provided. |
| # See https://bugs.python.org/issue16308 |
| try: |
| func = args.func |
| except AttributeError: |
| # No arguments or subcommands were given. |
| parser.print_help() |
| parser.exit() |
| |
| # Dispatch to the right subcommand |
| args.func(args) |