Index: llvm/trunk/utils/git-svn/git-llvm =================================================================== --- llvm/trunk/utils/git-svn/git-llvm +++ llvm/trunk/utils/git-svn/git-llvm @@ -0,0 +1,278 @@ +#!/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 + + +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 shell(cmd, strip=True, cwd=None, stdin=None): + log_verbose('Running: %s' % ' '.join(cmd)) + + start = time.time() + p = subprocess.Popen(cmd, cwd=cwd, stdout=subprocess.PIPE, + stderr=subprocess.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: + if stderr: + eprint('`%s` printed to stderr:' % ' '.join(cmd)) + eprint(stderr.rstrip()) + if strip: + stdout = stdout.rstrip('\r\n') + return stdout + eprint('`%s` returned %s' % (' '.join(cmd), p.returncode)) + if stderr: + eprint(stderr.rstrip()) + sys.exit(2) + + +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)) + + +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 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: + diff = git('show', '--binary', rev, '--', sr, strip=False) + svn_sr_path = os.path.join(svn_repo, GIT_TO_SVN_DIR[sr]) + # git is the only thing that can handle its own patches... + log_verbose('Apply patch: %s' % diff) + shell(['git', 'apply', '-p2', '-'], cwd=svn_sr_path, stdin=diff) + + 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)) + 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. + svn_root = os.path.join(git_root, '.git', '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)