3 # ======- git-llvm - LLVM Git Help Integration ---------*- python -*--========#
5 # The LLVM Compiler Infrastructure
7 # This file is distributed under the University of Illinois Open Source
8 # License. See LICENSE.TXT for details.
10 # ==------------------------------------------------------------------------==#
16 This file provides integration for git.
19 from __future__ import print_function
30 assert sys.version_info >= (2, 7)
33 # It's *almost* a straightforward mapping from the monorepo to svn...
55 GIT_TO_SVN_DIR.update({'clang': 'cfe/trunk'})
62 def eprint(*args, **kwargs):
63 print(*args, file=sys.stderr, **kwargs)
66 def log(*args, **kwargs):
69 print(*args, **kwargs)
72 def log_verbose(*args, **kwargs):
75 print(*args, **kwargs)
85 (head, tail) = os.path.split(d)
86 if not head or head == '/':
92 """Lazily create a /dev/null fd for use in shell()"""
94 if dev_null_fd is None:
95 dev_null_fd = open(os.devnull, 'w')
99 def shell(cmd, strip=True, cwd=None, stdin=None, die_on_failure=True,
100 ignore_errors=False):
101 log_verbose('Running: %s' % ' '.join(cmd))
103 err_pipe = subprocess.PIPE
105 # Silence errors if requested.
106 err_pipe = get_dev_null()
109 p = subprocess.Popen(cmd, cwd=cwd, stdout=subprocess.PIPE, stderr=err_pipe,
110 stdin=subprocess.PIPE, universal_newlines=True)
111 stdout, stderr = p.communicate(input=stdin)
112 elapsed = time.time() - start
114 log_verbose('Command took %0.1fs' % elapsed)
116 if p.returncode == 0 or ignore_errors:
117 if stderr and not ignore_errors:
118 eprint('`%s` printed to stderr:' % ' '.join(cmd))
119 eprint(stderr.rstrip())
121 stdout = stdout.rstrip('\r\n')
123 err_msg = '`%s` returned %s' % (' '.join(cmd), p.returncode)
126 eprint(stderr.rstrip())
129 raise RuntimeError(err_msg)
132 def git(*cmd, **kwargs):
133 return shell(['git'] + list(cmd), kwargs.get('strip', True))
136 def svn(cwd, *cmd, **kwargs):
137 # TODO: Better way to do default arg when we have *cmd?
138 return shell(['svn'] + list(cmd), cwd=cwd, stdin=kwargs.get('stdin', None),
139 ignore_errors=kwargs.get('ignore_errors', None))
141 def program_exists(cmd):
142 if sys.platform == 'win32' and not cmd.endswith('.exe'):
144 for path in os.environ["PATH"].split(os.pathsep):
145 if os.access(os.path.join(path, cmd), os.X_OK):
149 def get_default_rev_range():
150 # Get the branch tracked by the current branch, as set by
151 # git branch --set-upstream-to See http://serverfault.com/a/352236/38694.
152 cur_branch = git('rev-parse', '--symbolic-full-name', 'HEAD')
153 upstream_branch = git('for-each-ref', '--format=%(upstream:short)',
155 if not upstream_branch:
156 upstream_branch = 'origin/master'
158 # Get the newest common ancestor between HEAD and our upstream branch.
159 upstream_rev = git('merge-base', 'HEAD', upstream_branch)
160 return '%s..' % upstream_rev
163 def get_revs_to_push(rev_range):
165 rev_range = get_default_rev_range()
166 # Use git show rather than some plumbing command to figure out which revs
167 # are in rev_range because it handles single revs (HEAD^) and ranges
168 # (foo..bar) like we want.
169 revs = git('show', '--reverse', '--quiet',
170 '--pretty=%h', rev_range).splitlines()
172 die('Nothing to push: No revs in range %s.' % rev_range)
176 def clean_and_update_svn(svn_repo):
177 svn(svn_repo, 'revert', '-R', '.')
179 # Unfortunately it appears there's no svn equivalent for git clean, so we
180 # have to do it ourselves.
181 for line in svn(svn_repo, 'status', '--no-ignore').split('\n'):
182 if not line.startswith('?'):
184 filename = line[1:].strip()
185 os.remove(os.path.join(svn_repo, filename))
187 svn(svn_repo, 'update', *list(GIT_TO_SVN_DIR.values()))
190 def svn_init(svn_root):
191 if not os.path.exists(svn_root):
192 log('Creating svn staging directory: (%s)' % (svn_root))
193 os.makedirs(svn_root)
194 log('This is a one-time initialization, please be patient for a few'
196 svn(svn_root, 'checkout', '--depth=immediates',
197 'https://llvm.org/svn/llvm-project/', '.')
198 svn(svn_root, 'update', *list(GIT_TO_SVN_DIR.values()))
199 log("svn staging area ready in '%s'" % svn_root)
200 if not os.path.isdir(svn_root):
201 die("Can't initialize svn staging dir (%s)" % svn_root)
204 def fix_eol_style_native(rev, sr, svn_sr_path):
205 """Fix line endings before applying patches with Unix endings
207 SVN on Windows will check out files with CRLF for files with the
208 svn:eol-style property set to "native". This breaks `git apply`, which
209 typically works with Unix-line ending patches. Work around the problem here
210 by doing a dos2unix up front for files with svn:eol-style set to "native".
211 SVN will not commit a mass line ending re-doing because it detects the line
212 ending format for files with this property.
214 files = git('diff-tree', '--no-commit-id', '--name-only', '-r', rev, '--',
216 files = [f.split('/', 1)[1] for f in files]
217 # Skip files that don't exist in SVN yet.
218 files = [f for f in files if os.path.exists(os.path.join(svn_sr_path, f))]
219 # Use ignore_errors because 'svn propget' prints errors if the file doesn't
220 # have the named property. There doesn't seem to be a way to suppress that.
221 eol_props = svn(svn_sr_path, 'propget', 'svn:eol-style', *files,
225 # No need to split propget output on ' - ' when we have one file.
226 if eol_props.strip() == 'native':
229 for eol_prop in eol_props.split('\n'):
231 eol_prop = eol_prop.strip('\r')
234 prop_parts = eol_prop.rsplit(' - ', 1)
235 if len(prop_parts) != 2:
236 eprint("unable to parse svn propget line:")
239 (f, eol_style) = prop_parts
240 if eol_style == 'native':
242 # Reformat all files with native SVN line endings to Unix format. SVN knows
243 # files with native line endings are text files. It will commit just the
244 # diff, and not a mass line ending change.
245 shell(['dos2unix', '-q'] + crlf_files, cwd=svn_sr_path)
248 def svn_push_one_rev(svn_repo, rev, dry_run):
249 files = git('diff-tree', '--no-commit-id', '--name-only', '-r',
251 subrepos = {first_dirname(f) for f in files}
253 raise RuntimeError('Empty diff for rev %s?' % rev)
255 status = svn(svn_repo, 'status', '--no-ignore')
257 die("Can't push git rev %s because svn status is not empty:\n%s" %
261 svn_sr_path = os.path.join(svn_repo, GIT_TO_SVN_DIR[sr])
263 fix_eol_style_native(rev, sr, svn_sr_path)
264 diff = git('show', '--binary', rev, '--', sr, strip=False)
265 # git is the only thing that can handle its own patches...
266 log_verbose('Apply patch: %s' % diff)
268 shell(['git', 'apply', '-p2', '-'], cwd=svn_sr_path, stdin=diff,
269 die_on_failure=False)
270 except RuntimeError as e:
271 eprint("Patch doesn't apply: maybe you should try `git pull -r` "
275 status_lines = svn(svn_repo, 'status', '--no-ignore').split('\n')
277 for l in (l for l in status_lines if (l.startswith('?') or
279 svn(svn_repo, 'add', '--no-ignore', l[1:].strip())
280 for l in (l for l in status_lines if l.startswith('!')):
281 svn(svn_repo, 'remove', l[1:].strip())
283 # Now we're ready to commit.
284 commit_msg = git('show', '--pretty=%B', '--quiet', rev)
286 log(svn(svn_repo, 'commit', '-m', commit_msg, '--force-interactive'))
287 log('Committed %s to svn.' % rev)
289 log("Would have committed %s to svn, if this weren't a dry run." % rev)
293 '''Push changes back to SVN: this is extracted from Justin Lebar's script
294 available here: https://github.com/jlebar/llvm-repo-tools/
296 Note: a current limitation is that git does not track file rename, so they
297 will show up in SVN as delete+add.
300 git_root = git('rev-parse', '--show-toplevel')
301 if not os.path.isdir(git_root):
302 die("Can't find git root dir")
304 # Push from the root of the git repo
307 # We need a staging area for SVN, let's hide it in the .git directory.
308 dot_git_dir = git('rev-parse', '--git-common-dir')
309 svn_root = os.path.join(dot_git_dir, 'llvm-upstream-svn')
312 rev_range = args.rev_range
313 dry_run = args.dry_run
314 revs = get_revs_to_push(rev_range)
315 log('Pushing %d commit%s:\n%s' %
316 (len(revs), 's' if len(revs) != 1
317 else '', '\n'.join(' ' + git('show', '--oneline', '--quiet', c)
320 clean_and_update_svn(svn_root)
321 svn_push_one_rev(svn_root, r, dry_run)
324 if __name__ == '__main__':
325 if not program_exists('svn'):
326 die('error: git-llvm needs svn command, but svn is not installed.')
329 p = argparse.ArgumentParser(
330 prog='git llvm', formatter_class=argparse.RawDescriptionHelpFormatter,
332 subcommands = p.add_subparsers(title='subcommands',
333 description='valid subcommands',
334 help='additional help')
335 verbosity_group = p.add_mutually_exclusive_group()
336 verbosity_group.add_argument('-q', '--quiet', action='store_true',
337 help='print less information')
338 verbosity_group.add_argument('-v', '--verbose', action='store_true',
339 help='print more information')
341 parser_push = subcommands.add_parser(
342 'push', description=cmd_push.__doc__,
343 help='push changes back to the LLVM SVN repository')
344 parser_push.add_argument(
349 help='Do everything other than commit to svn. Leaves junk in the svn '
350 'repo, so probably will not work well if you try to commit more '
352 parser_push.add_argument(
357 help="revs to push (default: everything not in the branch's "
358 'upstream, or not in origin/master if the branch lacks '
359 'an explicit upstream)')
360 parser_push.set_defaults(func=cmd_push)
361 args = p.parse_args(argv)
362 VERBOSE = args.verbose
365 # Dispatch to the right subcommand