blob: bff06d5eeb4cb9602b6b2456f6da99d213c69225 [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',
Peter Collingbournef27f51d2017-06-04 22:18:57 +000039 'debuginfo-tests',
Mehdi Amini7b484632016-11-07 20:00:47 +000040 'dragonegg',
41 'klee',
42 'libclc',
43 'libcxx',
44 'libcxxabi',
Peter Collingbournef27f51d2017-06-04 22:18:57 +000045 'libunwind',
Mehdi Amini7b484632016-11-07 20:00:47 +000046 'lld',
47 'lldb',
Peter Collingbournef27f51d2017-06-04 22:18:57 +000048 'llgo',
Mehdi Amini7b484632016-11-07 20:00:47 +000049 'llvm',
Peter Collingbournef27f51d2017-06-04 22:18:57 +000050 'openmp',
51 'parallel-libs',
Mehdi Amini7b484632016-11-07 20:00:47 +000052 'polly',
James Y Knight12167822018-11-16 22:36:17 +000053 'pstl',
Mehdi Amini7b484632016-11-07 20:00:47 +000054 ]
55}
56GIT_TO_SVN_DIR.update({'clang': 'cfe/trunk'})
James Y Knight12167822018-11-16 22:36:17 +000057GIT_TO_SVN_DIR.update({'': 'monorepo-root/trunk'})
Mehdi Amini7b484632016-11-07 20:00:47 +000058
59VERBOSE = False
60QUIET = False
Reid Kleckner45340972017-04-24 22:09:08 +000061dev_null_fd = None
Mehdi Amini7b484632016-11-07 20:00:47 +000062
63
64def eprint(*args, **kwargs):
65 print(*args, file=sys.stderr, **kwargs)
66
67
68def log(*args, **kwargs):
69 if QUIET:
70 return
71 print(*args, **kwargs)
72
73
74def log_verbose(*args, **kwargs):
75 if not VERBOSE:
76 return
77 print(*args, **kwargs)
78
79
80def die(msg):
81 eprint(msg)
82 sys.exit(1)
83
84
James Y Knight12167822018-11-16 22:36:17 +000085def split_first_path_component(d):
86 # Assuming we have a git path, it'll use slashes even on windows...I hope.
87 if '/' in d:
88 return d.split('/', 1)
89 else:
90 return (d, None)
Mehdi Amini7b484632016-11-07 20:00:47 +000091
92
Reid Kleckner45340972017-04-24 22:09:08 +000093def get_dev_null():
94 """Lazily create a /dev/null fd for use in shell()"""
95 global dev_null_fd
96 if dev_null_fd is None:
97 dev_null_fd = open(os.devnull, 'w')
98 return dev_null_fd
99
100
101def shell(cmd, strip=True, cwd=None, stdin=None, die_on_failure=True,
Zachary Turnere5f47bb2018-10-09 23:42:28 +0000102 ignore_errors=False, force_binary_stdin=False):
James Y Knight12167822018-11-16 22:36:17 +0000103 log_verbose('Running in %s: %s' % (cwd, ' '.join(cmd)))
Mehdi Amini7b484632016-11-07 20:00:47 +0000104
Reid Kleckner45340972017-04-24 22:09:08 +0000105 err_pipe = subprocess.PIPE
106 if ignore_errors:
107 # Silence errors if requested.
108 err_pipe = get_dev_null()
109
Zachary Turnere5f47bb2018-10-09 23:42:28 +0000110 if force_binary_stdin and stdin:
111 stdin = stdin.encode('utf-8')
112
Mehdi Amini7b484632016-11-07 20:00:47 +0000113 start = time.time()
Zachary Turnere5f47bb2018-10-09 23:42:28 +0000114 text = not force_binary_stdin
Reid Kleckner45340972017-04-24 22:09:08 +0000115 p = subprocess.Popen(cmd, cwd=cwd, stdout=subprocess.PIPE, stderr=err_pipe,
Zachary Turnere5f47bb2018-10-09 23:42:28 +0000116 stdin=subprocess.PIPE, universal_newlines=text)
Mehdi Amini7b484632016-11-07 20:00:47 +0000117 stdout, stderr = p.communicate(input=stdin)
118 elapsed = time.time() - start
119
120 log_verbose('Command took %0.1fs' % elapsed)
121
Zachary Turnere5f47bb2018-10-09 23:42:28 +0000122 if not text:
123 stdout = stdout.decode('utf-8')
124 stderr = stderr.decode('utf-8')
125
Reid Kleckner45340972017-04-24 22:09:08 +0000126 if p.returncode == 0 or ignore_errors:
127 if stderr and not ignore_errors:
Mehdi Amini7b484632016-11-07 20:00:47 +0000128 eprint('`%s` printed to stderr:' % ' '.join(cmd))
129 eprint(stderr.rstrip())
130 if strip:
131 stdout = stdout.rstrip('\r\n')
James Y Knight12167822018-11-16 22:36:17 +0000132 if VERBOSE:
133 for l in stdout.splitlines():
134 log_verbose("STDOUT: %s" % l)
Mehdi Amini7b484632016-11-07 20:00:47 +0000135 return stdout
Mehdi Aminifbd26852016-11-12 01:17:59 +0000136 err_msg = '`%s` returned %s' % (' '.join(cmd), p.returncode)
137 eprint(err_msg)
Mehdi Amini7b484632016-11-07 20:00:47 +0000138 if stderr:
139 eprint(stderr.rstrip())
Mehdi Aminifbd26852016-11-12 01:17:59 +0000140 if die_on_failure:
141 sys.exit(2)
142 raise RuntimeError(err_msg)
Mehdi Amini7b484632016-11-07 20:00:47 +0000143
144
145def git(*cmd, **kwargs):
146 return shell(['git'] + list(cmd), kwargs.get('strip', True))
147
148
149def svn(cwd, *cmd, **kwargs):
150 # TODO: Better way to do default arg when we have *cmd?
Reid Kleckner45340972017-04-24 22:09:08 +0000151 return shell(['svn'] + list(cmd), cwd=cwd, stdin=kwargs.get('stdin', None),
152 ignore_errors=kwargs.get('ignore_errors', None))
Mehdi Amini7b484632016-11-07 20:00:47 +0000153
Rui Ueyama2f8db1d2017-05-23 21:50:40 +0000154def program_exists(cmd):
Zachary Turnerdc4cbc02017-05-24 00:28:46 +0000155 if sys.platform == 'win32' and not cmd.endswith('.exe'):
156 cmd += '.exe'
Rui Ueyama2f8db1d2017-05-23 21:50:40 +0000157 for path in os.environ["PATH"].split(os.pathsep):
158 if os.access(os.path.join(path, cmd), os.X_OK):
159 return True
160 return False
Mehdi Amini7b484632016-11-07 20:00:47 +0000161
162def get_default_rev_range():
163 # Get the branch tracked by the current branch, as set by
164 # git branch --set-upstream-to See http://serverfault.com/a/352236/38694.
165 cur_branch = git('rev-parse', '--symbolic-full-name', 'HEAD')
166 upstream_branch = git('for-each-ref', '--format=%(upstream:short)',
167 cur_branch)
168 if not upstream_branch:
169 upstream_branch = 'origin/master'
170
171 # Get the newest common ancestor between HEAD and our upstream branch.
172 upstream_rev = git('merge-base', 'HEAD', upstream_branch)
173 return '%s..' % upstream_rev
174
175
176def get_revs_to_push(rev_range):
177 if not rev_range:
178 rev_range = get_default_rev_range()
179 # Use git show rather than some plumbing command to figure out which revs
180 # are in rev_range because it handles single revs (HEAD^) and ranges
181 # (foo..bar) like we want.
182 revs = git('show', '--reverse', '--quiet',
183 '--pretty=%h', rev_range).splitlines()
184 if not revs:
185 die('Nothing to push: No revs in range %s.' % rev_range)
186 return revs
187
188
James Y Knight12167822018-11-16 22:36:17 +0000189def clean_svn(svn_repo):
Mehdi Amini7b484632016-11-07 20:00:47 +0000190 svn(svn_repo, 'revert', '-R', '.')
191
192 # Unfortunately it appears there's no svn equivalent for git clean, so we
193 # have to do it ourselves.
Walter Leeb074f142017-12-22 21:19:13 +0000194 for line in svn(svn_repo, 'status', '--no-ignore').split('\n'):
Mehdi Amini7b484632016-11-07 20:00:47 +0000195 if not line.startswith('?'):
196 continue
197 filename = line[1:].strip()
198 os.remove(os.path.join(svn_repo, filename))
199
Mehdi Amini7b484632016-11-07 20:00:47 +0000200
201def svn_init(svn_root):
202 if not os.path.exists(svn_root):
203 log('Creating svn staging directory: (%s)' % (svn_root))
204 os.makedirs(svn_root)
Mehdi Amini7b484632016-11-07 20:00:47 +0000205 svn(svn_root, 'checkout', '--depth=immediates',
206 'https://llvm.org/svn/llvm-project/', '.')
Mehdi Amini7b484632016-11-07 20:00:47 +0000207 log("svn staging area ready in '%s'" % svn_root)
208 if not os.path.isdir(svn_root):
209 die("Can't initialize svn staging dir (%s)" % svn_root)
210
211
James Y Knight12167822018-11-16 22:36:17 +0000212def fix_eol_style_native(rev, svn_sr_path, files):
Reid Kleckner45340972017-04-24 22:09:08 +0000213 """Fix line endings before applying patches with Unix endings
214
215 SVN on Windows will check out files with CRLF for files with the
216 svn:eol-style property set to "native". This breaks `git apply`, which
217 typically works with Unix-line ending patches. Work around the problem here
218 by doing a dos2unix up front for files with svn:eol-style set to "native".
219 SVN will not commit a mass line ending re-doing because it detects the line
220 ending format for files with this property.
221 """
Reid Kleckner162c5cd2017-05-18 17:17:17 +0000222 # Skip files that don't exist in SVN yet.
223 files = [f for f in files if os.path.exists(os.path.join(svn_sr_path, f))]
Reid Kleckner45340972017-04-24 22:09:08 +0000224 # Use ignore_errors because 'svn propget' prints errors if the file doesn't
225 # have the named property. There doesn't seem to be a way to suppress that.
226 eol_props = svn(svn_sr_path, 'propget', 'svn:eol-style', *files,
Reid Kleckner0f442bc2017-05-12 00:10:19 +0000227 ignore_errors=True)
Reid Kleckner45340972017-04-24 22:09:08 +0000228 crlf_files = []
Reid Kleckner0f442bc2017-05-12 00:10:19 +0000229 if len(files) == 1:
230 # No need to split propget output on ' - ' when we have one file.
Zachary Turnere5f47bb2018-10-09 23:42:28 +0000231 if eol_props.strip() in ['native', 'CRLF']:
Reid Kleckner0f442bc2017-05-12 00:10:19 +0000232 crlf_files = files
233 else:
234 for eol_prop in eol_props.split('\n'):
235 # Remove spare CR.
236 eol_prop = eol_prop.strip('\r')
237 if not eol_prop:
238 continue
239 prop_parts = eol_prop.rsplit(' - ', 1)
240 if len(prop_parts) != 2:
241 eprint("unable to parse svn propget line:")
242 eprint(eol_prop)
243 continue
244 (f, eol_style) = prop_parts
245 if eol_style == 'native':
246 crlf_files.append(f)
Zachary Turnere5f47bb2018-10-09 23:42:28 +0000247 if crlf_files:
James Y Knight12167822018-11-16 22:36:17 +0000248 # Reformat all files with native SVN line endings to Unix format. SVN
249 # knows files with native line endings are text files. It will commit
250 # just the diff, and not a mass line ending change.
Zachary Turnere5f47bb2018-10-09 23:42:28 +0000251 shell(['dos2unix'] + crlf_files, ignore_errors=True, cwd=svn_sr_path)
Reid Kleckner45340972017-04-24 22:09:08 +0000252
James Y Knight12167822018-11-16 22:36:17 +0000253def get_all_parent_dirs(name):
254 parts = []
255 head, tail = os.path.split(name)
256 while head:
257 parts.append(head)
258 head, tail = os.path.split(head)
259 return parts
260
261def split_subrepo(f):
262 # Given a path, splits it into (subproject, rest-of-path). If the path is
263 # not in a subproject, returns ('', full-path).
264
265 subproject, remainder = split_first_path_component(f)
266
267 if subproject in GIT_TO_SVN_DIR:
268 return subproject, remainder
269 else:
270 return '', f
271
Mehdi Amini7b484632016-11-07 20:00:47 +0000272def svn_push_one_rev(svn_repo, rev, dry_run):
273 files = git('diff-tree', '--no-commit-id', '--name-only', '-r',
274 rev).split('\n')
James Y Knight12167822018-11-16 22:36:17 +0000275 if not files:
Mehdi Amini7b484632016-11-07 20:00:47 +0000276 raise RuntimeError('Empty diff for rev %s?' % rev)
277
James Y Knight12167822018-11-16 22:36:17 +0000278 # Split files by subrepo
279 subrepo_files = collections.defaultdict(list)
280 for f in files:
281 subrepo, remainder = split_subrepo(f)
282 subrepo_files[subrepo].append(remainder)
283
Walter Leeb074f142017-12-22 21:19:13 +0000284 status = svn(svn_repo, 'status', '--no-ignore')
Mehdi Amini7b484632016-11-07 20:00:47 +0000285 if status:
286 die("Can't push git rev %s because svn status is not empty:\n%s" %
287 (rev, status))
288
James Y Knight12167822018-11-16 22:36:17 +0000289 svn_dirs_to_update = set()
290 for sr, files in subrepo_files.iteritems():
291 svn_sr_path = GIT_TO_SVN_DIR[sr]
292 for f in files:
293 svn_dirs_to_update.update(
294 get_all_parent_dirs(os.path.join(svn_sr_path, f)))
295
296 # Sort by length to ensure that the parent directories are passed to svn
297 # before child directories.
298 sorted_dirs_to_update = sorted(svn_dirs_to_update,
299 cmp=lambda x,y: cmp(len(x), len(y)))
300
301 # SVN update only in the affected directories.
302 svn(svn_repo, 'update', '--depth=immediates', *sorted_dirs_to_update)
303
304 for sr, files in subrepo_files.iteritems():
Mehdi Amini7b484632016-11-07 20:00:47 +0000305 svn_sr_path = os.path.join(svn_repo, GIT_TO_SVN_DIR[sr])
Reid Kleckner45340972017-04-24 22:09:08 +0000306 if os.name == 'nt':
James Y Knight12167822018-11-16 22:36:17 +0000307 fix_eol_style_native(rev, svn_sr_path, files)
308 diff = git('show', '--binary', rev, '--',
309 *(os.path.join(sr, f) for f in files),
310 strip=False)
Mehdi Amini7b484632016-11-07 20:00:47 +0000311 # git is the only thing that can handle its own patches...
312 log_verbose('Apply patch: %s' % diff)
James Y Knight12167822018-11-16 22:36:17 +0000313 if sr == '':
314 prefix_strip = '-p1'
315 else:
316 prefix_strip = '-p2'
Mehdi Aminifbd26852016-11-12 01:17:59 +0000317 try:
James Y Knight12167822018-11-16 22:36:17 +0000318 # If we allow python to apply the diff in text mode, it will
319 # silently convert \n to \r\n which git doesn't like.
320 shell(['git', 'apply', prefix_strip, '-'], cwd=svn_sr_path,
321 stdin=diff, die_on_failure=False, force_binary_stdin=True)
Mehdi Aminifbd26852016-11-12 01:17:59 +0000322 except RuntimeError as e:
323 eprint("Patch doesn't apply: maybe you should try `git pull -r` "
324 "first?")
325 sys.exit(2)
Mehdi Amini7b484632016-11-07 20:00:47 +0000326
Walter Leeb074f142017-12-22 21:19:13 +0000327 status_lines = svn(svn_repo, 'status', '--no-ignore').split('\n')
Mehdi Amini7b484632016-11-07 20:00:47 +0000328
Walter Leeb074f142017-12-22 21:19:13 +0000329 for l in (l for l in status_lines if (l.startswith('?') or
330 l.startswith('I'))):
331 svn(svn_repo, 'add', '--no-ignore', l[1:].strip())
Mehdi Amini7b484632016-11-07 20:00:47 +0000332 for l in (l for l in status_lines if l.startswith('!')):
333 svn(svn_repo, 'remove', l[1:].strip())
334
335 # Now we're ready to commit.
336 commit_msg = git('show', '--pretty=%B', '--quiet', rev)
337 if not dry_run:
Mehdi Amini5c289b72016-11-30 19:12:53 +0000338 log(svn(svn_repo, 'commit', '-m', commit_msg, '--force-interactive'))
Mehdi Amini7b484632016-11-07 20:00:47 +0000339 log('Committed %s to svn.' % rev)
340 else:
341 log("Would have committed %s to svn, if this weren't a dry run." % rev)
342
343
344def cmd_push(args):
345 '''Push changes back to SVN: this is extracted from Justin Lebar's script
346 available here: https://github.com/jlebar/llvm-repo-tools/
347
348 Note: a current limitation is that git does not track file rename, so they
349 will show up in SVN as delete+add.
350 '''
351 # Get the git root
352 git_root = git('rev-parse', '--show-toplevel')
353 if not os.path.isdir(git_root):
354 die("Can't find git root dir")
355
356 # Push from the root of the git repo
357 os.chdir(git_root)
358
359 # We need a staging area for SVN, let's hide it in the .git directory.
Mehdi Aminif95a4592016-11-07 20:35:02 +0000360 dot_git_dir = git('rev-parse', '--git-common-dir')
361 svn_root = os.path.join(dot_git_dir, 'llvm-upstream-svn')
Mehdi Amini7b484632016-11-07 20:00:47 +0000362 svn_init(svn_root)
363
364 rev_range = args.rev_range
365 dry_run = args.dry_run
366 revs = get_revs_to_push(rev_range)
367 log('Pushing %d commit%s:\n%s' %
368 (len(revs), 's' if len(revs) != 1
369 else '', '\n'.join(' ' + git('show', '--oneline', '--quiet', c)
370 for c in revs)))
371 for r in revs:
James Y Knight12167822018-11-16 22:36:17 +0000372 clean_svn(svn_root)
Mehdi Amini7b484632016-11-07 20:00:47 +0000373 svn_push_one_rev(svn_root, r, dry_run)
374
375
376if __name__ == '__main__':
Rui Ueyama2f8db1d2017-05-23 21:50:40 +0000377 if not program_exists('svn'):
378 die('error: git-llvm needs svn command, but svn is not installed.')
379
Mehdi Amini7b484632016-11-07 20:00:47 +0000380 argv = sys.argv[1:]
381 p = argparse.ArgumentParser(
382 prog='git llvm', formatter_class=argparse.RawDescriptionHelpFormatter,
383 description=__doc__)
384 subcommands = p.add_subparsers(title='subcommands',
385 description='valid subcommands',
386 help='additional help')
387 verbosity_group = p.add_mutually_exclusive_group()
388 verbosity_group.add_argument('-q', '--quiet', action='store_true',
389 help='print less information')
390 verbosity_group.add_argument('-v', '--verbose', action='store_true',
391 help='print more information')
392
393 parser_push = subcommands.add_parser(
394 'push', description=cmd_push.__doc__,
395 help='push changes back to the LLVM SVN repository')
396 parser_push.add_argument(
397 '-n',
398 '--dry-run',
399 dest='dry_run',
400 action='store_true',
401 help='Do everything other than commit to svn. Leaves junk in the svn '
402 'repo, so probably will not work well if you try to commit more '
403 'than one rev.')
404 parser_push.add_argument(
405 'rev_range',
406 metavar='GIT_REVS',
407 type=str,
408 nargs='?',
409 help="revs to push (default: everything not in the branch's "
410 'upstream, or not in origin/master if the branch lacks '
411 'an explicit upstream)')
412 parser_push.set_defaults(func=cmd_push)
413 args = p.parse_args(argv)
414 VERBOSE = args.verbose
415 QUIET = args.quiet
416
417 # Dispatch to the right subcommand
418 args.func(args)