blob: 11a5719cac750cbbf38944fcd6df988df1a78633 [file] [log] [blame]
Mehdi Amini7b484632016-11-07 20:00:47 +00001#!/usr/bin/env python
2#
3# ======- git-llvm - LLVM Git Help Integration ---------*- python -*--========#
4#
5# The LLVM Compiler Infrastructure
6#
7# This file is distributed under the University of Illinois Open Source
8# License. See LICENSE.TXT for details.
9#
10# ==------------------------------------------------------------------------==#
11
12"""
13git-llvm integration
14====================
15
16This file provides integration for git.
17"""
18
19from __future__ import print_function
20import argparse
21import collections
22import contextlib
23import errno
24import os
25import re
26import subprocess
27import sys
28import tempfile
29import time
30assert sys.version_info >= (2, 7)
31
32
33# It's *almost* a straightforward mapping from the monorepo to svn...
34GIT_TO_SVN_DIR = {
35 d: (d + '/trunk')
36 for d in [
37 'clang-tools-extra',
38 'compiler-rt',
39 'dragonegg',
40 'klee',
41 'libclc',
42 'libcxx',
43 'libcxxabi',
44 'lld',
45 'lldb',
46 'llvm',
47 'polly',
48 ]
49}
50GIT_TO_SVN_DIR.update({'clang': 'cfe/trunk'})
51
52VERBOSE = False
53QUIET = False
54
55
56def eprint(*args, **kwargs):
57 print(*args, file=sys.stderr, **kwargs)
58
59
60def log(*args, **kwargs):
61 if QUIET:
62 return
63 print(*args, **kwargs)
64
65
66def log_verbose(*args, **kwargs):
67 if not VERBOSE:
68 return
69 print(*args, **kwargs)
70
71
72def die(msg):
73 eprint(msg)
74 sys.exit(1)
75
76
77def first_dirname(d):
78 while True:
79 (head, tail) = os.path.split(d)
80 if not head or head == '/':
81 return tail
82 d = head
83
84
Mehdi Aminifbd26852016-11-12 01:17:59 +000085def shell(cmd, strip=True, cwd=None, stdin=None, die_on_failure=True):
Mehdi Amini7b484632016-11-07 20:00:47 +000086 log_verbose('Running: %s' % ' '.join(cmd))
87
88 start = time.time()
89 p = subprocess.Popen(cmd, cwd=cwd, stdout=subprocess.PIPE,
90 stderr=subprocess.PIPE, stdin=subprocess.PIPE)
91 stdout, stderr = p.communicate(input=stdin)
92 elapsed = time.time() - start
93
94 log_verbose('Command took %0.1fs' % elapsed)
95
96 if p.returncode == 0:
97 if stderr:
98 eprint('`%s` printed to stderr:' % ' '.join(cmd))
99 eprint(stderr.rstrip())
100 if strip:
101 stdout = stdout.rstrip('\r\n')
102 return stdout
Mehdi Aminifbd26852016-11-12 01:17:59 +0000103 err_msg = '`%s` returned %s' % (' '.join(cmd), p.returncode)
104 eprint(err_msg)
Mehdi Amini7b484632016-11-07 20:00:47 +0000105 if stderr:
106 eprint(stderr.rstrip())
Mehdi Aminifbd26852016-11-12 01:17:59 +0000107 if die_on_failure:
108 sys.exit(2)
109 raise RuntimeError(err_msg)
Mehdi Amini7b484632016-11-07 20:00:47 +0000110
111
112def git(*cmd, **kwargs):
113 return shell(['git'] + list(cmd), kwargs.get('strip', True))
114
115
116def svn(cwd, *cmd, **kwargs):
117 # TODO: Better way to do default arg when we have *cmd?
118 return shell(['svn'] + list(cmd), cwd=cwd, stdin=kwargs.get('stdin', None))
119
120
121def get_default_rev_range():
122 # Get the branch tracked by the current branch, as set by
123 # git branch --set-upstream-to See http://serverfault.com/a/352236/38694.
124 cur_branch = git('rev-parse', '--symbolic-full-name', 'HEAD')
125 upstream_branch = git('for-each-ref', '--format=%(upstream:short)',
126 cur_branch)
127 if not upstream_branch:
128 upstream_branch = 'origin/master'
129
130 # Get the newest common ancestor between HEAD and our upstream branch.
131 upstream_rev = git('merge-base', 'HEAD', upstream_branch)
132 return '%s..' % upstream_rev
133
134
135def get_revs_to_push(rev_range):
136 if not rev_range:
137 rev_range = get_default_rev_range()
138 # Use git show rather than some plumbing command to figure out which revs
139 # are in rev_range because it handles single revs (HEAD^) and ranges
140 # (foo..bar) like we want.
141 revs = git('show', '--reverse', '--quiet',
142 '--pretty=%h', rev_range).splitlines()
143 if not revs:
144 die('Nothing to push: No revs in range %s.' % rev_range)
145 return revs
146
147
148def clean_and_update_svn(svn_repo):
149 svn(svn_repo, 'revert', '-R', '.')
150
151 # Unfortunately it appears there's no svn equivalent for git clean, so we
152 # have to do it ourselves.
153 for line in svn(svn_repo, 'status').split('\n'):
154 if not line.startswith('?'):
155 continue
156 filename = line[1:].strip()
157 os.remove(os.path.join(svn_repo, filename))
158
159 svn(svn_repo, 'update', *list(GIT_TO_SVN_DIR.values()))
160
161
162def svn_init(svn_root):
163 if not os.path.exists(svn_root):
164 log('Creating svn staging directory: (%s)' % (svn_root))
165 os.makedirs(svn_root)
166 log('This is a one-time initialization, please be patient for a few '
167 ' minutes...')
168 svn(svn_root, 'checkout', '--depth=immediates',
169 'https://llvm.org/svn/llvm-project/', '.')
170 svn(svn_root, 'update', *list(GIT_TO_SVN_DIR.values()))
171 log("svn staging area ready in '%s'" % svn_root)
172 if not os.path.isdir(svn_root):
173 die("Can't initialize svn staging dir (%s)" % svn_root)
174
175
176def svn_push_one_rev(svn_repo, rev, dry_run):
177 files = git('diff-tree', '--no-commit-id', '--name-only', '-r',
178 rev).split('\n')
179 subrepos = {first_dirname(f) for f in files}
180 if not subrepos:
181 raise RuntimeError('Empty diff for rev %s?' % rev)
182
183 status = svn(svn_repo, 'status')
184 if status:
185 die("Can't push git rev %s because svn status is not empty:\n%s" %
186 (rev, status))
187
188 for sr in subrepos:
189 diff = git('show', '--binary', rev, '--', sr, strip=False)
190 svn_sr_path = os.path.join(svn_repo, GIT_TO_SVN_DIR[sr])
191 # git is the only thing that can handle its own patches...
192 log_verbose('Apply patch: %s' % diff)
Mehdi Aminifbd26852016-11-12 01:17:59 +0000193 try:
194 shell(['git', 'apply', '-p2', '-'], cwd=svn_sr_path, stdin=diff,
195 die_on_failure=False)
196 except RuntimeError as e:
197 eprint("Patch doesn't apply: maybe you should try `git pull -r` "
198 "first?")
199 sys.exit(2)
Mehdi Amini7b484632016-11-07 20:00:47 +0000200
201 status_lines = svn(svn_repo, 'status').split('\n')
202
203 for l in (l for l in status_lines if l.startswith('?')):
204 svn(svn_repo, 'add', l[1:].strip())
205 for l in (l for l in status_lines if l.startswith('!')):
206 svn(svn_repo, 'remove', l[1:].strip())
207
208 # Now we're ready to commit.
209 commit_msg = git('show', '--pretty=%B', '--quiet', rev)
210 if not dry_run:
Mehdi Amini5c289b72016-11-30 19:12:53 +0000211 log(svn(svn_repo, 'commit', '-m', commit_msg, '--force-interactive'))
Mehdi Amini7b484632016-11-07 20:00:47 +0000212 log('Committed %s to svn.' % rev)
213 else:
214 log("Would have committed %s to svn, if this weren't a dry run." % rev)
215
216
217def cmd_push(args):
218 '''Push changes back to SVN: this is extracted from Justin Lebar's script
219 available here: https://github.com/jlebar/llvm-repo-tools/
220
221 Note: a current limitation is that git does not track file rename, so they
222 will show up in SVN as delete+add.
223 '''
224 # Get the git root
225 git_root = git('rev-parse', '--show-toplevel')
226 if not os.path.isdir(git_root):
227 die("Can't find git root dir")
228
229 # Push from the root of the git repo
230 os.chdir(git_root)
231
232 # We need a staging area for SVN, let's hide it in the .git directory.
Mehdi Aminif95a4592016-11-07 20:35:02 +0000233 dot_git_dir = git('rev-parse', '--git-common-dir')
234 svn_root = os.path.join(dot_git_dir, 'llvm-upstream-svn')
Mehdi Amini7b484632016-11-07 20:00:47 +0000235 svn_init(svn_root)
236
237 rev_range = args.rev_range
238 dry_run = args.dry_run
239 revs = get_revs_to_push(rev_range)
240 log('Pushing %d commit%s:\n%s' %
241 (len(revs), 's' if len(revs) != 1
242 else '', '\n'.join(' ' + git('show', '--oneline', '--quiet', c)
243 for c in revs)))
244 for r in revs:
245 clean_and_update_svn(svn_root)
246 svn_push_one_rev(svn_root, r, dry_run)
247
248
249if __name__ == '__main__':
250 argv = sys.argv[1:]
251 p = argparse.ArgumentParser(
252 prog='git llvm', formatter_class=argparse.RawDescriptionHelpFormatter,
253 description=__doc__)
254 subcommands = p.add_subparsers(title='subcommands',
255 description='valid subcommands',
256 help='additional help')
257 verbosity_group = p.add_mutually_exclusive_group()
258 verbosity_group.add_argument('-q', '--quiet', action='store_true',
259 help='print less information')
260 verbosity_group.add_argument('-v', '--verbose', action='store_true',
261 help='print more information')
262
263 parser_push = subcommands.add_parser(
264 'push', description=cmd_push.__doc__,
265 help='push changes back to the LLVM SVN repository')
266 parser_push.add_argument(
267 '-n',
268 '--dry-run',
269 dest='dry_run',
270 action='store_true',
271 help='Do everything other than commit to svn. Leaves junk in the svn '
272 'repo, so probably will not work well if you try to commit more '
273 'than one rev.')
274 parser_push.add_argument(
275 'rev_range',
276 metavar='GIT_REVS',
277 type=str,
278 nargs='?',
279 help="revs to push (default: everything not in the branch's "
280 'upstream, or not in origin/master if the branch lacks '
281 'an explicit upstream)')
282 parser_push.set_defaults(func=cmd_push)
283 args = p.parse_args(argv)
284 VERBOSE = args.verbose
285 QUIET = args.quiet
286
287 # Dispatch to the right subcommand
288 args.func(args)