blob: bf234301f1321c1ce370b751a961e3e534cc9a20 [file] [log] [blame]
Mehdi Amini7e34ddb2016-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
James Y Knight83bf61b2018-11-16 23:59:23 +000032try:
33 dict.iteritems
34except AttributeError:
35 # Python 3
36 def iteritems(d):
37 return iter(d.items())
38else:
39 # Python 2
40 def iteritems(d):
41 return d.iteritems()
Mehdi Amini7e34ddb2016-11-07 20:00:47 +000042
43# It's *almost* a straightforward mapping from the monorepo to svn...
44GIT_TO_SVN_DIR = {
Tom Stellardf5990792019-03-21 20:45:59 +000045 d: (d + '/branches/release_80')
Mehdi Amini7e34ddb2016-11-07 20:00:47 +000046 for d in [
47 'clang-tools-extra',
48 'compiler-rt',
Peter Collingbourne6ef4e402017-06-04 22:18:57 +000049 'debuginfo-tests',
Mehdi Amini7e34ddb2016-11-07 20:00:47 +000050 'dragonegg',
51 'klee',
52 'libclc',
53 'libcxx',
54 'libcxxabi',
Peter Collingbourne6ef4e402017-06-04 22:18:57 +000055 'libunwind',
Mehdi Amini7e34ddb2016-11-07 20:00:47 +000056 'lld',
57 'lldb',
Peter Collingbourne6ef4e402017-06-04 22:18:57 +000058 'llgo',
Mehdi Amini7e34ddb2016-11-07 20:00:47 +000059 'llvm',
Peter Collingbourne6ef4e402017-06-04 22:18:57 +000060 'openmp',
61 'parallel-libs',
Mehdi Amini7e34ddb2016-11-07 20:00:47 +000062 'polly',
James Y Knight54652752018-11-16 22:36:17 +000063 'pstl',
Mehdi Amini7e34ddb2016-11-07 20:00:47 +000064 ]
65}
Tom Stellardf5990792019-03-21 20:45:59 +000066GIT_TO_SVN_DIR.update({'clang': 'cfe/branches/release_80'})
67GIT_TO_SVN_DIR.update({'': 'monorepo-root/branches/release_80'})
Mehdi Amini7e34ddb2016-11-07 20:00:47 +000068
69VERBOSE = False
70QUIET = False
Reid Klecknerd403ad12017-04-24 22:09:08 +000071dev_null_fd = None
Mehdi Amini7e34ddb2016-11-07 20:00:47 +000072
73
74def eprint(*args, **kwargs):
75 print(*args, file=sys.stderr, **kwargs)
76
77
78def log(*args, **kwargs):
79 if QUIET:
80 return
81 print(*args, **kwargs)
82
83
84def log_verbose(*args, **kwargs):
85 if not VERBOSE:
86 return
87 print(*args, **kwargs)
88
89
90def die(msg):
91 eprint(msg)
92 sys.exit(1)
93
94
James Y Knight54652752018-11-16 22:36:17 +000095def split_first_path_component(d):
96 # Assuming we have a git path, it'll use slashes even on windows...I hope.
97 if '/' in d:
98 return d.split('/', 1)
99 else:
100 return (d, None)
Mehdi Amini7e34ddb2016-11-07 20:00:47 +0000101
102
Reid Klecknerd403ad12017-04-24 22:09:08 +0000103def get_dev_null():
104 """Lazily create a /dev/null fd for use in shell()"""
105 global dev_null_fd
106 if dev_null_fd is None:
107 dev_null_fd = open(os.devnull, 'w')
108 return dev_null_fd
109
110
111def shell(cmd, strip=True, cwd=None, stdin=None, die_on_failure=True,
James Y Knightdad8def2018-11-28 15:30:39 +0000112 ignore_errors=False, text=True):
James Y Knight54652752018-11-16 22:36:17 +0000113 log_verbose('Running in %s: %s' % (cwd, ' '.join(cmd)))
Mehdi Amini7e34ddb2016-11-07 20:00:47 +0000114
Reid Klecknerd403ad12017-04-24 22:09:08 +0000115 err_pipe = subprocess.PIPE
116 if ignore_errors:
117 # Silence errors if requested.
118 err_pipe = get_dev_null()
119
Mehdi Amini7e34ddb2016-11-07 20:00:47 +0000120 start = time.time()
Reid Klecknerd403ad12017-04-24 22:09:08 +0000121 p = subprocess.Popen(cmd, cwd=cwd, stdout=subprocess.PIPE, stderr=err_pipe,
James Y Knightdad8def2018-11-28 15:30:39 +0000122 stdin=subprocess.PIPE,
123 universal_newlines=text)
Mehdi Amini7e34ddb2016-11-07 20:00:47 +0000124 stdout, stderr = p.communicate(input=stdin)
125 elapsed = time.time() - start
126
127 log_verbose('Command took %0.1fs' % elapsed)
128
Reid Klecknerd403ad12017-04-24 22:09:08 +0000129 if p.returncode == 0 or ignore_errors:
130 if stderr and not ignore_errors:
Mehdi Amini7e34ddb2016-11-07 20:00:47 +0000131 eprint('`%s` printed to stderr:' % ' '.join(cmd))
132 eprint(stderr.rstrip())
133 if strip:
James Y Knightdad8def2018-11-28 15:30:39 +0000134 if text:
135 stdout = stdout.rstrip('\r\n')
136 else:
137 stdout = stdout.rstrip(b'\r\n')
James Y Knight54652752018-11-16 22:36:17 +0000138 if VERBOSE:
139 for l in stdout.splitlines():
140 log_verbose("STDOUT: %s" % l)
Mehdi Amini7e34ddb2016-11-07 20:00:47 +0000141 return stdout
Mehdi Amini85244672016-11-12 01:17:59 +0000142 err_msg = '`%s` returned %s' % (' '.join(cmd), p.returncode)
143 eprint(err_msg)
Mehdi Amini7e34ddb2016-11-07 20:00:47 +0000144 if stderr:
145 eprint(stderr.rstrip())
Mehdi Amini85244672016-11-12 01:17:59 +0000146 if die_on_failure:
147 sys.exit(2)
148 raise RuntimeError(err_msg)
Mehdi Amini7e34ddb2016-11-07 20:00:47 +0000149
150
151def git(*cmd, **kwargs):
James Y Knightdad8def2018-11-28 15:30:39 +0000152 return shell(['git'] + list(cmd), **kwargs)
Mehdi Amini7e34ddb2016-11-07 20:00:47 +0000153
154
155def svn(cwd, *cmd, **kwargs):
James Y Knightdad8def2018-11-28 15:30:39 +0000156 return shell(['svn'] + list(cmd), cwd=cwd, **kwargs)
Mehdi Amini7e34ddb2016-11-07 20:00:47 +0000157
Rui Ueyama30395dd2017-05-23 21:50:40 +0000158def program_exists(cmd):
Zachary Turner69916e12017-05-24 00:28:46 +0000159 if sys.platform == 'win32' and not cmd.endswith('.exe'):
160 cmd += '.exe'
Rui Ueyama30395dd2017-05-23 21:50:40 +0000161 for path in os.environ["PATH"].split(os.pathsep):
162 if os.access(os.path.join(path, cmd), os.X_OK):
163 return True
164 return False
Mehdi Amini7e34ddb2016-11-07 20:00:47 +0000165
166def get_default_rev_range():
167 # Get the branch tracked by the current branch, as set by
168 # git branch --set-upstream-to See http://serverfault.com/a/352236/38694.
169 cur_branch = git('rev-parse', '--symbolic-full-name', 'HEAD')
170 upstream_branch = git('for-each-ref', '--format=%(upstream:short)',
171 cur_branch)
172 if not upstream_branch:
173 upstream_branch = 'origin/master'
174
175 # Get the newest common ancestor between HEAD and our upstream branch.
176 upstream_rev = git('merge-base', 'HEAD', upstream_branch)
177 return '%s..' % upstream_rev
178
179
180def get_revs_to_push(rev_range):
181 if not rev_range:
182 rev_range = get_default_rev_range()
183 # Use git show rather than some plumbing command to figure out which revs
184 # are in rev_range because it handles single revs (HEAD^) and ranges
185 # (foo..bar) like we want.
186 revs = git('show', '--reverse', '--quiet',
187 '--pretty=%h', rev_range).splitlines()
188 if not revs:
189 die('Nothing to push: No revs in range %s.' % rev_range)
190 return revs
191
192
James Y Knight54652752018-11-16 22:36:17 +0000193def clean_svn(svn_repo):
Mehdi Amini7e34ddb2016-11-07 20:00:47 +0000194 svn(svn_repo, 'revert', '-R', '.')
195
196 # Unfortunately it appears there's no svn equivalent for git clean, so we
197 # have to do it ourselves.
Walter Lee4e7b8c02017-12-22 21:19:13 +0000198 for line in svn(svn_repo, 'status', '--no-ignore').split('\n'):
Mehdi Amini7e34ddb2016-11-07 20:00:47 +0000199 if not line.startswith('?'):
200 continue
201 filename = line[1:].strip()
202 os.remove(os.path.join(svn_repo, filename))
203
Mehdi Amini7e34ddb2016-11-07 20:00:47 +0000204
205def svn_init(svn_root):
206 if not os.path.exists(svn_root):
207 log('Creating svn staging directory: (%s)' % (svn_root))
208 os.makedirs(svn_root)
James Y Knightdad8def2018-11-28 15:30:39 +0000209 svn(svn_root, 'checkout', '--depth=empty',
Mehdi Amini7e34ddb2016-11-07 20:00:47 +0000210 'https://llvm.org/svn/llvm-project/', '.')
Mehdi Amini7e34ddb2016-11-07 20:00:47 +0000211 log("svn staging area ready in '%s'" % svn_root)
212 if not os.path.isdir(svn_root):
213 die("Can't initialize svn staging dir (%s)" % svn_root)
214
215
James Y Knight54652752018-11-16 22:36:17 +0000216def fix_eol_style_native(rev, svn_sr_path, files):
Reid Klecknerd403ad12017-04-24 22:09:08 +0000217 """Fix line endings before applying patches with Unix endings
218
219 SVN on Windows will check out files with CRLF for files with the
220 svn:eol-style property set to "native". This breaks `git apply`, which
221 typically works with Unix-line ending patches. Work around the problem here
222 by doing a dos2unix up front for files with svn:eol-style set to "native".
223 SVN will not commit a mass line ending re-doing because it detects the line
224 ending format for files with this property.
225 """
Reid Kleckner71f28972017-05-18 17:17:17 +0000226 # Skip files that don't exist in SVN yet.
227 files = [f for f in files if os.path.exists(os.path.join(svn_sr_path, f))]
Reid Klecknerd403ad12017-04-24 22:09:08 +0000228 # Use ignore_errors because 'svn propget' prints errors if the file doesn't
229 # have the named property. There doesn't seem to be a way to suppress that.
230 eol_props = svn(svn_sr_path, 'propget', 'svn:eol-style', *files,
Reid Kleckner4dc3e3a2017-05-12 00:10:19 +0000231 ignore_errors=True)
Reid Klecknerd403ad12017-04-24 22:09:08 +0000232 crlf_files = []
Reid Kleckner4dc3e3a2017-05-12 00:10:19 +0000233 if len(files) == 1:
234 # No need to split propget output on ' - ' when we have one file.
Zachary Turner98649f62018-10-09 23:42:28 +0000235 if eol_props.strip() in ['native', 'CRLF']:
Reid Kleckner4dc3e3a2017-05-12 00:10:19 +0000236 crlf_files = files
237 else:
238 for eol_prop in eol_props.split('\n'):
239 # Remove spare CR.
240 eol_prop = eol_prop.strip('\r')
241 if not eol_prop:
242 continue
243 prop_parts = eol_prop.rsplit(' - ', 1)
244 if len(prop_parts) != 2:
245 eprint("unable to parse svn propget line:")
246 eprint(eol_prop)
247 continue
248 (f, eol_style) = prop_parts
249 if eol_style == 'native':
250 crlf_files.append(f)
Zachary Turner98649f62018-10-09 23:42:28 +0000251 if crlf_files:
James Y Knight54652752018-11-16 22:36:17 +0000252 # Reformat all files with native SVN line endings to Unix format. SVN
253 # knows files with native line endings are text files. It will commit
254 # just the diff, and not a mass line ending change.
Zachary Turner98649f62018-10-09 23:42:28 +0000255 shell(['dos2unix'] + crlf_files, ignore_errors=True, cwd=svn_sr_path)
Reid Klecknerd403ad12017-04-24 22:09:08 +0000256
James Y Knight54652752018-11-16 22:36:17 +0000257def split_subrepo(f):
258 # Given a path, splits it into (subproject, rest-of-path). If the path is
259 # not in a subproject, returns ('', full-path).
260
261 subproject, remainder = split_first_path_component(f)
262
263 if subproject in GIT_TO_SVN_DIR:
264 return subproject, remainder
265 else:
266 return '', f
267
James Y Knight97ddb1a2018-11-29 16:46:34 +0000268def get_all_parent_dirs(name):
269 parts = []
270 head, tail = os.path.split(name)
271 while head:
272 parts.append(head)
273 head, tail = os.path.split(head)
274 return parts
275
Mehdi Amini7e34ddb2016-11-07 20:00:47 +0000276def svn_push_one_rev(svn_repo, rev, dry_run):
277 files = git('diff-tree', '--no-commit-id', '--name-only', '-r',
278 rev).split('\n')
James Y Knight54652752018-11-16 22:36:17 +0000279 if not files:
Mehdi Amini7e34ddb2016-11-07 20:00:47 +0000280 raise RuntimeError('Empty diff for rev %s?' % rev)
281
James Y Knight54652752018-11-16 22:36:17 +0000282 # Split files by subrepo
283 subrepo_files = collections.defaultdict(list)
284 for f in files:
285 subrepo, remainder = split_subrepo(f)
286 subrepo_files[subrepo].append(remainder)
287
Walter Lee4e7b8c02017-12-22 21:19:13 +0000288 status = svn(svn_repo, 'status', '--no-ignore')
Mehdi Amini7e34ddb2016-11-07 20:00:47 +0000289 if status:
290 die("Can't push git rev %s because svn status is not empty:\n%s" %
291 (rev, status))
292
James Y Knight54652752018-11-16 22:36:17 +0000293 svn_dirs_to_update = set()
James Y Knight83bf61b2018-11-16 23:59:23 +0000294 for sr, files in iteritems(subrepo_files):
James Y Knight54652752018-11-16 22:36:17 +0000295 svn_sr_path = GIT_TO_SVN_DIR[sr]
296 for f in files:
James Y Knightdad8def2018-11-28 15:30:39 +0000297 svn_dirs_to_update.add(
298 os.path.dirname(os.path.join(svn_sr_path, f)))
James Y Knight54652752018-11-16 22:36:17 +0000299
James Y Knight97ddb1a2018-11-29 16:46:34 +0000300 # We also need to svn update any parent directories which are not yet present
301 parent_dirs = set()
302 for dir in svn_dirs_to_update:
303 parent_dirs.update(get_all_parent_dirs(dir))
304 parent_dirs = set(dir for dir in parent_dirs
305 if not os.path.exists(os.path.join(svn_repo, dir)))
306 svn_dirs_to_update.update(parent_dirs)
307
308 # Sort by length to ensure that the parent directories are passed to svn
309 # before child directories.
310 sorted_dirs_to_update = sorted(svn_dirs_to_update, key=len)
311
James Y Knight54652752018-11-16 22:36:17 +0000312 # SVN update only in the affected directories.
James Y Knight97ddb1a2018-11-29 16:46:34 +0000313 svn(svn_repo, 'update', '--depth=files', *sorted_dirs_to_update)
James Y Knight54652752018-11-16 22:36:17 +0000314
James Y Knight83bf61b2018-11-16 23:59:23 +0000315 for sr, files in iteritems(subrepo_files):
Mehdi Amini7e34ddb2016-11-07 20:00:47 +0000316 svn_sr_path = os.path.join(svn_repo, GIT_TO_SVN_DIR[sr])
Reid Klecknerd403ad12017-04-24 22:09:08 +0000317 if os.name == 'nt':
James Y Knight54652752018-11-16 22:36:17 +0000318 fix_eol_style_native(rev, svn_sr_path, files)
James Y Knightdad8def2018-11-28 15:30:39 +0000319 # We use text=False (and pass '--binary') so that we can get an exact
320 # diff that can be passed as-is to 'git apply' without any line ending,
321 # encoding, or other mangling.
James Y Knight54652752018-11-16 22:36:17 +0000322 diff = git('show', '--binary', rev, '--',
323 *(os.path.join(sr, f) for f in files),
James Y Knightdad8def2018-11-28 15:30:39 +0000324 strip=False, text=False)
Mehdi Amini7e34ddb2016-11-07 20:00:47 +0000325 # git is the only thing that can handle its own patches...
James Y Knight54652752018-11-16 22:36:17 +0000326 if sr == '':
327 prefix_strip = '-p1'
328 else:
329 prefix_strip = '-p2'
Mehdi Amini85244672016-11-12 01:17:59 +0000330 try:
James Y Knight54652752018-11-16 22:36:17 +0000331 shell(['git', 'apply', prefix_strip, '-'], cwd=svn_sr_path,
James Y Knightdad8def2018-11-28 15:30:39 +0000332 stdin=diff, die_on_failure=False, text=False)
Mehdi Amini85244672016-11-12 01:17:59 +0000333 except RuntimeError as e:
334 eprint("Patch doesn't apply: maybe you should try `git pull -r` "
335 "first?")
336 sys.exit(2)
Mehdi Amini7e34ddb2016-11-07 20:00:47 +0000337
Walter Lee4e7b8c02017-12-22 21:19:13 +0000338 status_lines = svn(svn_repo, 'status', '--no-ignore').split('\n')
Mehdi Amini7e34ddb2016-11-07 20:00:47 +0000339
Walter Lee4e7b8c02017-12-22 21:19:13 +0000340 for l in (l for l in status_lines if (l.startswith('?') or
341 l.startswith('I'))):
342 svn(svn_repo, 'add', '--no-ignore', l[1:].strip())
Mehdi Amini7e34ddb2016-11-07 20:00:47 +0000343 for l in (l for l in status_lines if l.startswith('!')):
344 svn(svn_repo, 'remove', l[1:].strip())
345
346 # Now we're ready to commit.
347 commit_msg = git('show', '--pretty=%B', '--quiet', rev)
348 if not dry_run:
Mehdi Amini19ef8ff2016-11-30 19:12:53 +0000349 log(svn(svn_repo, 'commit', '-m', commit_msg, '--force-interactive'))
Mehdi Amini7e34ddb2016-11-07 20:00:47 +0000350 log('Committed %s to svn.' % rev)
351 else:
352 log("Would have committed %s to svn, if this weren't a dry run." % rev)
353
354
355def cmd_push(args):
356 '''Push changes back to SVN: this is extracted from Justin Lebar's script
357 available here: https://github.com/jlebar/llvm-repo-tools/
358
359 Note: a current limitation is that git does not track file rename, so they
360 will show up in SVN as delete+add.
361 '''
362 # Get the git root
363 git_root = git('rev-parse', '--show-toplevel')
364 if not os.path.isdir(git_root):
365 die("Can't find git root dir")
366
367 # Push from the root of the git repo
368 os.chdir(git_root)
369
370 # We need a staging area for SVN, let's hide it in the .git directory.
Mehdi Amini59bbd3c2016-11-07 20:35:02 +0000371 dot_git_dir = git('rev-parse', '--git-common-dir')
372 svn_root = os.path.join(dot_git_dir, 'llvm-upstream-svn')
Mehdi Amini7e34ddb2016-11-07 20:00:47 +0000373 svn_init(svn_root)
374
375 rev_range = args.rev_range
376 dry_run = args.dry_run
377 revs = get_revs_to_push(rev_range)
378 log('Pushing %d commit%s:\n%s' %
379 (len(revs), 's' if len(revs) != 1
380 else '', '\n'.join(' ' + git('show', '--oneline', '--quiet', c)
381 for c in revs)))
382 for r in revs:
James Y Knight54652752018-11-16 22:36:17 +0000383 clean_svn(svn_root)
Mehdi Amini7e34ddb2016-11-07 20:00:47 +0000384 svn_push_one_rev(svn_root, r, dry_run)
385
386
387if __name__ == '__main__':
Rui Ueyama30395dd2017-05-23 21:50:40 +0000388 if not program_exists('svn'):
389 die('error: git-llvm needs svn command, but svn is not installed.')
390
Mehdi Amini7e34ddb2016-11-07 20:00:47 +0000391 argv = sys.argv[1:]
392 p = argparse.ArgumentParser(
393 prog='git llvm', formatter_class=argparse.RawDescriptionHelpFormatter,
394 description=__doc__)
395 subcommands = p.add_subparsers(title='subcommands',
396 description='valid subcommands',
397 help='additional help')
398 verbosity_group = p.add_mutually_exclusive_group()
399 verbosity_group.add_argument('-q', '--quiet', action='store_true',
400 help='print less information')
401 verbosity_group.add_argument('-v', '--verbose', action='store_true',
402 help='print more information')
403
404 parser_push = subcommands.add_parser(
405 'push', description=cmd_push.__doc__,
406 help='push changes back to the LLVM SVN repository')
407 parser_push.add_argument(
408 '-n',
409 '--dry-run',
410 dest='dry_run',
411 action='store_true',
412 help='Do everything other than commit to svn. Leaves junk in the svn '
413 'repo, so probably will not work well if you try to commit more '
414 'than one rev.')
415 parser_push.add_argument(
416 'rev_range',
417 metavar='GIT_REVS',
418 type=str,
419 nargs='?',
420 help="revs to push (default: everything not in the branch's "
421 'upstream, or not in origin/master if the branch lacks '
422 'an explicit upstream)')
423 parser_push.set_defaults(func=cmd_push)
424 args = p.parse_args(argv)
425 VERBOSE = args.verbose
426 QUIET = args.quiet
427
428 # Dispatch to the right subcommand
429 args.func(args)