blob: c2eaa5b6e64041f3618f4ecaecf1170cb6d3ccd7 [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
Reid Kleckner45340972017-04-24 22:09:08 +000054dev_null_fd = None
Mehdi Amini7b484632016-11-07 20:00:47 +000055
56
57def eprint(*args, **kwargs):
58 print(*args, file=sys.stderr, **kwargs)
59
60
61def log(*args, **kwargs):
62 if QUIET:
63 return
64 print(*args, **kwargs)
65
66
67def log_verbose(*args, **kwargs):
68 if not VERBOSE:
69 return
70 print(*args, **kwargs)
71
72
73def die(msg):
74 eprint(msg)
75 sys.exit(1)
76
77
78def first_dirname(d):
79 while True:
80 (head, tail) = os.path.split(d)
81 if not head or head == '/':
82 return tail
83 d = head
84
85
Reid Kleckner45340972017-04-24 22:09:08 +000086def get_dev_null():
87 """Lazily create a /dev/null fd for use in shell()"""
88 global dev_null_fd
89 if dev_null_fd is None:
90 dev_null_fd = open(os.devnull, 'w')
91 return dev_null_fd
92
93
94def shell(cmd, strip=True, cwd=None, stdin=None, die_on_failure=True,
95 ignore_errors=False):
Mehdi Amini7b484632016-11-07 20:00:47 +000096 log_verbose('Running: %s' % ' '.join(cmd))
97
Reid Kleckner45340972017-04-24 22:09:08 +000098 err_pipe = subprocess.PIPE
99 if ignore_errors:
100 # Silence errors if requested.
101 err_pipe = get_dev_null()
102
Mehdi Amini7b484632016-11-07 20:00:47 +0000103 start = time.time()
Reid Kleckner45340972017-04-24 22:09:08 +0000104 p = subprocess.Popen(cmd, cwd=cwd, stdout=subprocess.PIPE, stderr=err_pipe,
105 stdin=subprocess.PIPE)
Mehdi Amini7b484632016-11-07 20:00:47 +0000106 stdout, stderr = p.communicate(input=stdin)
107 elapsed = time.time() - start
108
109 log_verbose('Command took %0.1fs' % elapsed)
110
Reid Kleckner45340972017-04-24 22:09:08 +0000111 if p.returncode == 0 or ignore_errors:
112 if stderr and not ignore_errors:
Mehdi Amini7b484632016-11-07 20:00:47 +0000113 eprint('`%s` printed to stderr:' % ' '.join(cmd))
114 eprint(stderr.rstrip())
115 if strip:
116 stdout = stdout.rstrip('\r\n')
117 return stdout
Mehdi Aminifbd26852016-11-12 01:17:59 +0000118 err_msg = '`%s` returned %s' % (' '.join(cmd), p.returncode)
119 eprint(err_msg)
Mehdi Amini7b484632016-11-07 20:00:47 +0000120 if stderr:
121 eprint(stderr.rstrip())
Mehdi Aminifbd26852016-11-12 01:17:59 +0000122 if die_on_failure:
123 sys.exit(2)
124 raise RuntimeError(err_msg)
Mehdi Amini7b484632016-11-07 20:00:47 +0000125
126
127def git(*cmd, **kwargs):
128 return shell(['git'] + list(cmd), kwargs.get('strip', True))
129
130
131def svn(cwd, *cmd, **kwargs):
132 # TODO: Better way to do default arg when we have *cmd?
Reid Kleckner45340972017-04-24 22:09:08 +0000133 return shell(['svn'] + list(cmd), cwd=cwd, stdin=kwargs.get('stdin', None),
134 ignore_errors=kwargs.get('ignore_errors', None))
Mehdi Amini7b484632016-11-07 20:00:47 +0000135
136
137def get_default_rev_range():
138 # Get the branch tracked by the current branch, as set by
139 # git branch --set-upstream-to See http://serverfault.com/a/352236/38694.
140 cur_branch = git('rev-parse', '--symbolic-full-name', 'HEAD')
141 upstream_branch = git('for-each-ref', '--format=%(upstream:short)',
142 cur_branch)
143 if not upstream_branch:
144 upstream_branch = 'origin/master'
145
146 # Get the newest common ancestor between HEAD and our upstream branch.
147 upstream_rev = git('merge-base', 'HEAD', upstream_branch)
148 return '%s..' % upstream_rev
149
150
151def get_revs_to_push(rev_range):
152 if not rev_range:
153 rev_range = get_default_rev_range()
154 # Use git show rather than some plumbing command to figure out which revs
155 # are in rev_range because it handles single revs (HEAD^) and ranges
156 # (foo..bar) like we want.
157 revs = git('show', '--reverse', '--quiet',
158 '--pretty=%h', rev_range).splitlines()
159 if not revs:
160 die('Nothing to push: No revs in range %s.' % rev_range)
161 return revs
162
163
164def clean_and_update_svn(svn_repo):
165 svn(svn_repo, 'revert', '-R', '.')
166
167 # Unfortunately it appears there's no svn equivalent for git clean, so we
168 # have to do it ourselves.
169 for line in svn(svn_repo, 'status').split('\n'):
170 if not line.startswith('?'):
171 continue
172 filename = line[1:].strip()
173 os.remove(os.path.join(svn_repo, filename))
174
175 svn(svn_repo, 'update', *list(GIT_TO_SVN_DIR.values()))
176
177
178def svn_init(svn_root):
179 if not os.path.exists(svn_root):
180 log('Creating svn staging directory: (%s)' % (svn_root))
181 os.makedirs(svn_root)
Rui Ueyama62839f02016-12-20 05:49:56 +0000182 log('This is a one-time initialization, please be patient for a few'
Mehdi Amini7b484632016-11-07 20:00:47 +0000183 ' minutes...')
184 svn(svn_root, 'checkout', '--depth=immediates',
185 'https://llvm.org/svn/llvm-project/', '.')
186 svn(svn_root, 'update', *list(GIT_TO_SVN_DIR.values()))
187 log("svn staging area ready in '%s'" % svn_root)
188 if not os.path.isdir(svn_root):
189 die("Can't initialize svn staging dir (%s)" % svn_root)
190
191
Reid Kleckner45340972017-04-24 22:09:08 +0000192def fix_eol_style_native(rev, sr, svn_sr_path):
193 """Fix line endings before applying patches with Unix endings
194
195 SVN on Windows will check out files with CRLF for files with the
196 svn:eol-style property set to "native". This breaks `git apply`, which
197 typically works with Unix-line ending patches. Work around the problem here
198 by doing a dos2unix up front for files with svn:eol-style set to "native".
199 SVN will not commit a mass line ending re-doing because it detects the line
200 ending format for files with this property.
201 """
202 files = git('diff-tree', '--no-commit-id', '--name-only', '-r', rev, '--',
203 sr).split('\n')
204 files = [f.split('/', 1)[1] for f in files]
205 # Use ignore_errors because 'svn propget' prints errors if the file doesn't
206 # have the named property. There doesn't seem to be a way to suppress that.
207 eol_props = svn(svn_sr_path, 'propget', 'svn:eol-style', *files,
208 ignore_errors=True).split('\n')
209 crlf_files = []
210 for eol_prop in eol_props:
Reid Klecknerdf726352017-04-24 22:26:46 +0000211 # Remove spare CR.
212 eol_prop = eol_prop.strip('\r')
Reid Kleckner45340972017-04-24 22:09:08 +0000213 if not eol_prop:
214 continue
215 prop_parts = eol_prop.rsplit(' - ', 1)
216 if len(prop_parts) != 2:
217 eprint("unable to parse svn propget line:")
218 eprint(eol_prop)
219 continue
220 (f, eol_style) = prop_parts
221 if eol_style == 'native':
222 crlf_files.append(f)
223 # Reformat all files with native SVN line endings to Unix format. SVN knows
224 # files with native line endings are text files. It will commit just the
225 # diff, and not a mass line ending change.
226 shell(['dos2unix', '-q'] + crlf_files, cwd=svn_sr_path)
227
228
Mehdi Amini7b484632016-11-07 20:00:47 +0000229def svn_push_one_rev(svn_repo, rev, dry_run):
230 files = git('diff-tree', '--no-commit-id', '--name-only', '-r',
231 rev).split('\n')
232 subrepos = {first_dirname(f) for f in files}
233 if not subrepos:
234 raise RuntimeError('Empty diff for rev %s?' % rev)
235
236 status = svn(svn_repo, 'status')
237 if status:
238 die("Can't push git rev %s because svn status is not empty:\n%s" %
239 (rev, status))
240
241 for sr in subrepos:
Mehdi Amini7b484632016-11-07 20:00:47 +0000242 svn_sr_path = os.path.join(svn_repo, GIT_TO_SVN_DIR[sr])
Reid Kleckner45340972017-04-24 22:09:08 +0000243 if os.name == 'nt':
244 fix_eol_style_native(rev, sr, svn_sr_path)
245 diff = git('show', '--binary', rev, '--', sr, strip=False)
Mehdi Amini7b484632016-11-07 20:00:47 +0000246 # git is the only thing that can handle its own patches...
247 log_verbose('Apply patch: %s' % diff)
Mehdi Aminifbd26852016-11-12 01:17:59 +0000248 try:
249 shell(['git', 'apply', '-p2', '-'], cwd=svn_sr_path, stdin=diff,
250 die_on_failure=False)
251 except RuntimeError as e:
252 eprint("Patch doesn't apply: maybe you should try `git pull -r` "
253 "first?")
254 sys.exit(2)
Mehdi Amini7b484632016-11-07 20:00:47 +0000255
256 status_lines = svn(svn_repo, 'status').split('\n')
257
258 for l in (l for l in status_lines if l.startswith('?')):
259 svn(svn_repo, 'add', l[1:].strip())
260 for l in (l for l in status_lines if l.startswith('!')):
261 svn(svn_repo, 'remove', l[1:].strip())
262
263 # Now we're ready to commit.
264 commit_msg = git('show', '--pretty=%B', '--quiet', rev)
265 if not dry_run:
Mehdi Amini5c289b72016-11-30 19:12:53 +0000266 log(svn(svn_repo, 'commit', '-m', commit_msg, '--force-interactive'))
Mehdi Amini7b484632016-11-07 20:00:47 +0000267 log('Committed %s to svn.' % rev)
268 else:
269 log("Would have committed %s to svn, if this weren't a dry run." % rev)
270
271
272def cmd_push(args):
273 '''Push changes back to SVN: this is extracted from Justin Lebar's script
274 available here: https://github.com/jlebar/llvm-repo-tools/
275
276 Note: a current limitation is that git does not track file rename, so they
277 will show up in SVN as delete+add.
278 '''
279 # Get the git root
280 git_root = git('rev-parse', '--show-toplevel')
281 if not os.path.isdir(git_root):
282 die("Can't find git root dir")
283
284 # Push from the root of the git repo
285 os.chdir(git_root)
286
287 # We need a staging area for SVN, let's hide it in the .git directory.
Mehdi Aminif95a4592016-11-07 20:35:02 +0000288 dot_git_dir = git('rev-parse', '--git-common-dir')
289 svn_root = os.path.join(dot_git_dir, 'llvm-upstream-svn')
Mehdi Amini7b484632016-11-07 20:00:47 +0000290 svn_init(svn_root)
291
292 rev_range = args.rev_range
293 dry_run = args.dry_run
294 revs = get_revs_to_push(rev_range)
295 log('Pushing %d commit%s:\n%s' %
296 (len(revs), 's' if len(revs) != 1
297 else '', '\n'.join(' ' + git('show', '--oneline', '--quiet', c)
298 for c in revs)))
299 for r in revs:
300 clean_and_update_svn(svn_root)
301 svn_push_one_rev(svn_root, r, dry_run)
302
303
304if __name__ == '__main__':
305 argv = sys.argv[1:]
306 p = argparse.ArgumentParser(
307 prog='git llvm', formatter_class=argparse.RawDescriptionHelpFormatter,
308 description=__doc__)
309 subcommands = p.add_subparsers(title='subcommands',
310 description='valid subcommands',
311 help='additional help')
312 verbosity_group = p.add_mutually_exclusive_group()
313 verbosity_group.add_argument('-q', '--quiet', action='store_true',
314 help='print less information')
315 verbosity_group.add_argument('-v', '--verbose', action='store_true',
316 help='print more information')
317
318 parser_push = subcommands.add_parser(
319 'push', description=cmd_push.__doc__,
320 help='push changes back to the LLVM SVN repository')
321 parser_push.add_argument(
322 '-n',
323 '--dry-run',
324 dest='dry_run',
325 action='store_true',
326 help='Do everything other than commit to svn. Leaves junk in the svn '
327 'repo, so probably will not work well if you try to commit more '
328 'than one rev.')
329 parser_push.add_argument(
330 'rev_range',
331 metavar='GIT_REVS',
332 type=str,
333 nargs='?',
334 help="revs to push (default: everything not in the branch's "
335 'upstream, or not in origin/master if the branch lacks '
336 'an explicit upstream)')
337 parser_push.set_defaults(func=cmd_push)
338 args = p.parse_args(argv)
339 VERBOSE = args.verbose
340 QUIET = args.quiet
341
342 # Dispatch to the right subcommand
343 args.func(args)