#!/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 This will push changes from the current HEAD to the 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://{}'.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): # 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(), '{}:{}'.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) 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( '-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)