forked from OSchip/llvm-project
				
			
		
			
				
	
	
		
			349 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable File
		
	
	
			
		
		
	
	
			349 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable File
		
	
	
#!/usr/bin/env python
 | 
						|
#
 | 
						|
# ======- git-llvm - LLVM Git Help Integration ---------*- python -*--========#
 | 
						|
#
 | 
						|
#                     The LLVM Compiler Infrastructure
 | 
						|
#
 | 
						|
# This file is distributed under the University of Illinois Open Source
 | 
						|
# License. See LICENSE.TXT for details.
 | 
						|
#
 | 
						|
# ==------------------------------------------------------------------------==#
 | 
						|
 | 
						|
"""
 | 
						|
git-llvm integration
 | 
						|
====================
 | 
						|
 | 
						|
This file provides integration for git.
 | 
						|
"""
 | 
						|
 | 
						|
from __future__ import print_function
 | 
						|
import argparse
 | 
						|
import collections
 | 
						|
import contextlib
 | 
						|
import errno
 | 
						|
import os
 | 
						|
import re
 | 
						|
import subprocess
 | 
						|
import sys
 | 
						|
import tempfile
 | 
						|
import time
 | 
						|
assert sys.version_info >= (2, 7)
 | 
						|
 | 
						|
 | 
						|
# It's *almost* a straightforward mapping from the monorepo to svn...
 | 
						|
GIT_TO_SVN_DIR = {
 | 
						|
    d: (d + '/trunk')
 | 
						|
    for d in [
 | 
						|
        'clang-tools-extra',
 | 
						|
        'compiler-rt',
 | 
						|
        'dragonegg',
 | 
						|
        'klee',
 | 
						|
        'libclc',
 | 
						|
        'libcxx',
 | 
						|
        'libcxxabi',
 | 
						|
        'lld',
 | 
						|
        'lldb',
 | 
						|
        'llvm',
 | 
						|
        'polly',
 | 
						|
    ]
 | 
						|
}
 | 
						|
GIT_TO_SVN_DIR.update({'clang': 'cfe/trunk'})
 | 
						|
 | 
						|
VERBOSE = False
 | 
						|
QUIET = False
 | 
						|
dev_null_fd = None
 | 
						|
 | 
						|
 | 
						|
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 first_dirname(d):
 | 
						|
    while True:
 | 
						|
        (head, tail) = os.path.split(d)
 | 
						|
        if not head or head == '/':
 | 
						|
            return tail
 | 
						|
        d = head
 | 
						|
 | 
						|
 | 
						|
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):
 | 
						|
    log_verbose('Running: %s' % ' '.join(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)
 | 
						|
    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:
 | 
						|
            eprint('`%s` printed to stderr:' % ' '.join(cmd))
 | 
						|
            eprint(stderr.rstrip())
 | 
						|
        if strip:
 | 
						|
            stdout = stdout.rstrip('\r\n')
 | 
						|
        return stdout
 | 
						|
    err_msg = '`%s` returned %s' % (' '.join(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.get('strip', True))
 | 
						|
 | 
						|
 | 
						|
def svn(cwd, *cmd, **kwargs):
 | 
						|
    # TODO: Better way to do default arg when we have *cmd?
 | 
						|
    return shell(['svn'] + list(cmd), cwd=cwd, stdin=kwargs.get('stdin', None),
 | 
						|
                 ignore_errors=kwargs.get('ignore_errors', None))
 | 
						|
 | 
						|
 | 
						|
def get_default_rev_range():
 | 
						|
    # Get the branch tracked by the current branch, as set by
 | 
						|
    # git branch --set-upstream-to  See http://serverfault.com/a/352236/38694.
 | 
						|
    cur_branch = git('rev-parse', '--symbolic-full-name', 'HEAD')
 | 
						|
    upstream_branch = git('for-each-ref', '--format=%(upstream:short)',
 | 
						|
                          cur_branch)
 | 
						|
    if not upstream_branch:
 | 
						|
        upstream_branch = 'origin/master'
 | 
						|
 | 
						|
    # Get the newest common ancestor between HEAD and our upstream branch.
 | 
						|
    upstream_rev = git('merge-base', 'HEAD', upstream_branch)
 | 
						|
    return '%s..' % upstream_rev
 | 
						|
 | 
						|
 | 
						|
def get_revs_to_push(rev_range):
 | 
						|
    if not rev_range:
 | 
						|
        rev_range = get_default_rev_range()
 | 
						|
    # Use git show rather than some plumbing command to figure out which revs
 | 
						|
    # are in rev_range because it handles single revs (HEAD^) and ranges
 | 
						|
    # (foo..bar) like we want.
 | 
						|
    revs = git('show', '--reverse', '--quiet',
 | 
						|
               '--pretty=%h', rev_range).splitlines()
 | 
						|
    if not revs:
 | 
						|
        die('Nothing to push: No revs in range %s.' % rev_range)
 | 
						|
    return revs
 | 
						|
 | 
						|
 | 
						|
def clean_and_update_svn(svn_repo):
 | 
						|
    svn(svn_repo, 'revert', '-R', '.')
 | 
						|
 | 
						|
    # Unfortunately it appears there's no svn equivalent for git clean, so we
 | 
						|
    # have to do it ourselves.
 | 
						|
    for line in svn(svn_repo, 'status').split('\n'):
 | 
						|
        if not line.startswith('?'):
 | 
						|
            continue
 | 
						|
        filename = line[1:].strip()
 | 
						|
        os.remove(os.path.join(svn_repo, filename))
 | 
						|
 | 
						|
    svn(svn_repo, 'update', *list(GIT_TO_SVN_DIR.values()))
 | 
						|
 | 
						|
 | 
						|
def svn_init(svn_root):
 | 
						|
    if not os.path.exists(svn_root):
 | 
						|
        log('Creating svn staging directory: (%s)' % (svn_root))
 | 
						|
        os.makedirs(svn_root)
 | 
						|
        log('This is a one-time initialization, please be patient for a few'
 | 
						|
            ' minutes...')
 | 
						|
        svn(svn_root, 'checkout', '--depth=immediates',
 | 
						|
            'https://llvm.org/svn/llvm-project/', '.')
 | 
						|
        svn(svn_root, 'update', *list(GIT_TO_SVN_DIR.values()))
 | 
						|
        log("svn staging area ready in '%s'" % svn_root)
 | 
						|
    if not os.path.isdir(svn_root):
 | 
						|
        die("Can't initialize svn staging dir (%s)" % svn_root)
 | 
						|
 | 
						|
 | 
						|
def fix_eol_style_native(rev, sr, svn_sr_path):
 | 
						|
    """Fix line endings before applying patches with Unix endings
 | 
						|
 | 
						|
    SVN on Windows will check out files with CRLF for files with the
 | 
						|
    svn:eol-style property set to "native". This breaks `git apply`, which
 | 
						|
    typically works with Unix-line ending patches. Work around the problem here
 | 
						|
    by doing a dos2unix up front for files with svn:eol-style set to "native".
 | 
						|
    SVN will not commit a mass line ending re-doing because it detects the line
 | 
						|
    ending format for files with this property.
 | 
						|
    """
 | 
						|
    files = git('diff-tree', '--no-commit-id', '--name-only', '-r', rev, '--',
 | 
						|
                sr).split('\n')
 | 
						|
    files = [f.split('/', 1)[1] for f in files]
 | 
						|
    # Use ignore_errors because 'svn propget' prints errors if the file doesn't
 | 
						|
    # have the named property. There doesn't seem to be a way to suppress that.
 | 
						|
    eol_props = svn(svn_sr_path, 'propget', 'svn:eol-style', *files,
 | 
						|
                    ignore_errors=True)
 | 
						|
    crlf_files = []
 | 
						|
    if len(files) == 1:
 | 
						|
        # No need to split propget output on ' - ' when we have one file.
 | 
						|
        if eol_props.strip() == 'native':
 | 
						|
            crlf_files = files
 | 
						|
    else:
 | 
						|
        for eol_prop in eol_props.split('\n'):
 | 
						|
            # Remove spare CR.
 | 
						|
            eol_prop = eol_prop.strip('\r')
 | 
						|
            if not eol_prop:
 | 
						|
                continue
 | 
						|
            prop_parts = eol_prop.rsplit(' - ', 1)
 | 
						|
            if len(prop_parts) != 2:
 | 
						|
                eprint("unable to parse svn propget line:")
 | 
						|
                eprint(eol_prop)
 | 
						|
                continue
 | 
						|
            (f, eol_style) = prop_parts
 | 
						|
            if eol_style == 'native':
 | 
						|
                crlf_files.append(f)
 | 
						|
    # Reformat all files with native SVN line endings to Unix format. SVN knows
 | 
						|
    # files with native line endings are text files. It will commit just the
 | 
						|
    # diff, and not a mass line ending change.
 | 
						|
    shell(['dos2unix', '-q'] + crlf_files, cwd=svn_sr_path)
 | 
						|
 | 
						|
 | 
						|
def svn_push_one_rev(svn_repo, rev, dry_run):
 | 
						|
    files = git('diff-tree', '--no-commit-id', '--name-only', '-r',
 | 
						|
                rev).split('\n')
 | 
						|
    subrepos = {first_dirname(f) for f in files}
 | 
						|
    if not subrepos:
 | 
						|
        raise RuntimeError('Empty diff for rev %s?' % rev)
 | 
						|
 | 
						|
    status = svn(svn_repo, 'status')
 | 
						|
    if status:
 | 
						|
        die("Can't push git rev %s because svn status is not empty:\n%s" %
 | 
						|
            (rev, status))
 | 
						|
 | 
						|
    for sr in subrepos:
 | 
						|
        svn_sr_path = os.path.join(svn_repo, GIT_TO_SVN_DIR[sr])
 | 
						|
        if os.name == 'nt':
 | 
						|
            fix_eol_style_native(rev, sr, svn_sr_path)
 | 
						|
        diff = git('show', '--binary', rev, '--', sr, strip=False)
 | 
						|
        # git is the only thing that can handle its own patches...
 | 
						|
        log_verbose('Apply patch: %s' % diff)
 | 
						|
        try:
 | 
						|
            shell(['git', 'apply', '-p2', '-'], cwd=svn_sr_path, stdin=diff,
 | 
						|
                  die_on_failure=False)
 | 
						|
        except RuntimeError as e:
 | 
						|
            eprint("Patch doesn't apply: maybe you should try `git pull -r` "
 | 
						|
                   "first?")
 | 
						|
            sys.exit(2)
 | 
						|
 | 
						|
    status_lines = svn(svn_repo, 'status').split('\n')
 | 
						|
 | 
						|
    for l in (l for l in status_lines if l.startswith('?')):
 | 
						|
        svn(svn_repo, 'add', l[1:].strip())
 | 
						|
    for l in (l for l in status_lines if l.startswith('!')):
 | 
						|
        svn(svn_repo, 'remove', l[1:].strip())
 | 
						|
 | 
						|
    # Now we're ready to commit.
 | 
						|
    commit_msg = git('show', '--pretty=%B', '--quiet', rev)
 | 
						|
    if not dry_run:
 | 
						|
        log(svn(svn_repo, 'commit', '-m', commit_msg, '--force-interactive'))
 | 
						|
        log('Committed %s to svn.' % rev)
 | 
						|
    else:
 | 
						|
        log("Would have committed %s to svn, if this weren't a dry run." % rev)
 | 
						|
 | 
						|
 | 
						|
def cmd_push(args):
 | 
						|
    '''Push changes back to SVN: this is extracted from Justin Lebar's script
 | 
						|
    available here: https://github.com/jlebar/llvm-repo-tools/
 | 
						|
 | 
						|
    Note: a current limitation is that git does not track file rename, so they
 | 
						|
    will show up in SVN as delete+add.
 | 
						|
    '''
 | 
						|
    # Get the git root
 | 
						|
    git_root = git('rev-parse', '--show-toplevel')
 | 
						|
    if not os.path.isdir(git_root):
 | 
						|
        die("Can't find git root dir")
 | 
						|
 | 
						|
    # Push from the root of the git repo
 | 
						|
    os.chdir(git_root)
 | 
						|
 | 
						|
    # We need a staging area for SVN, let's hide it in the .git directory.
 | 
						|
    dot_git_dir = git('rev-parse', '--git-common-dir')
 | 
						|
    svn_root = os.path.join(dot_git_dir, 'llvm-upstream-svn')
 | 
						|
    svn_init(svn_root)
 | 
						|
 | 
						|
    rev_range = args.rev_range
 | 
						|
    dry_run = args.dry_run
 | 
						|
    revs = get_revs_to_push(rev_range)
 | 
						|
    log('Pushing %d commit%s:\n%s' %
 | 
						|
        (len(revs), 's' if len(revs) != 1
 | 
						|
         else '', '\n'.join('  ' + git('show', '--oneline', '--quiet', c)
 | 
						|
                            for c in revs)))
 | 
						|
    for r in revs:
 | 
						|
        clean_and_update_svn(svn_root)
 | 
						|
        svn_push_one_rev(svn_root, r, dry_run)
 | 
						|
 | 
						|
 | 
						|
if __name__ == '__main__':
 | 
						|
    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(
 | 
						|
        'rev_range',
 | 
						|
        metavar='GIT_REVS',
 | 
						|
        type=str,
 | 
						|
        nargs='?',
 | 
						|
        help="revs to push (default: everything not in the branch's "
 | 
						|
        'upstream, or not in origin/master if the branch lacks '
 | 
						|
        'an explicit upstream)')
 | 
						|
    parser_push.set_defaults(func=cmd_push)
 | 
						|
    args = p.parse_args(argv)
 | 
						|
    VERBOSE = args.verbose
 | 
						|
    QUIET = args.quiet
 | 
						|
 | 
						|
    # Dispatch to the right subcommand
 | 
						|
    args.func(args)
 |