3 # ======- git-llvm - LLVM Git Help Integration ---------*- python -*--========#
5 # Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
6 # See https://llvm.org/LICENSE.txt for license information.
7 # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
9 # ==------------------------------------------------------------------------==#
15 This file provides integration for git.
18 from __future__
import print_function
29 assert sys
.version_info
>= (2, 7)
33 except AttributeError:
36 return iter(d
.items())
42 # It's *almost* a straightforward mapping from the monorepo to svn...
65 GIT_TO_SVN_DIR
.update({'clang': 'cfe/trunk'})
66 GIT_TO_SVN_DIR
.update({'': 'monorepo-root/trunk'})
73 def eprint(*args
, **kwargs
):
74 print(*args
, file=sys
.stderr
, **kwargs
)
77 def log(*args
, **kwargs
):
80 print(*args
, **kwargs
)
83 def log_verbose(*args
, **kwargs
):
86 print(*args
, **kwargs
)
94 def split_first_path_component(d
):
95 # Assuming we have a git path, it'll use slashes even on windows...I hope.
97 return d
.split('/', 1)
103 """Lazily create a /dev/null fd for use in shell()"""
105 if dev_null_fd
is None:
106 dev_null_fd
= open(os
.devnull
, 'w')
110 def shell(cmd
, strip
=True, cwd
=None, stdin
=None, die_on_failure
=True,
111 ignore_errors
=False, text
=True):
112 log_verbose('Running in %s: %s' % (cwd
, ' '.join(cmd
)))
114 err_pipe
= subprocess
.PIPE
116 # Silence errors if requested.
117 err_pipe
= get_dev_null()
120 p
= subprocess
.Popen(cmd
, cwd
=cwd
, stdout
=subprocess
.PIPE
, stderr
=err_pipe
,
121 stdin
=subprocess
.PIPE
,
122 universal_newlines
=text
)
123 stdout
, stderr
= p
.communicate(input=stdin
)
124 elapsed
= time
.time() - start
126 log_verbose('Command took %0.1fs' % elapsed
)
128 if p
.returncode
== 0 or ignore_errors
:
129 if stderr
and not ignore_errors
:
130 eprint('`%s` printed to stderr:' % ' '.join(cmd
))
131 eprint(stderr
.rstrip())
134 stdout
= stdout
.rstrip('\r\n')
136 stdout
= stdout
.rstrip(b
'\r\n')
138 for l
in stdout
.splitlines():
139 log_verbose("STDOUT: %s" % l
)
141 err_msg
= '`%s` returned %s' % (' '.join(cmd
), p
.returncode
)
144 eprint(stderr
.rstrip())
147 raise RuntimeError(err_msg
)
150 def git(*cmd
, **kwargs
):
151 return shell(['git'] + list(cmd
), **kwargs
)
154 def svn(cwd
, *cmd
, **kwargs
):
155 return shell(['svn'] + list(cmd
), cwd
=cwd
, **kwargs
)
157 def program_exists(cmd
):
158 if sys
.platform
== 'win32' and not cmd
.endswith('.exe'):
160 for path
in os
.environ
["PATH"].split(os
.pathsep
):
161 if os
.access(os
.path
.join(path
, cmd
), os
.X_OK
):
165 def get_default_rev_range():
166 # Get the branch tracked by the current branch, as set by
167 # git branch --set-upstream-to See http://serverfault.com/a/352236/38694.
168 cur_branch
= git('rev-parse', '--symbolic-full-name', 'HEAD')
169 upstream_branch
= git('for-each-ref', '--format=%(upstream:short)',
171 if not upstream_branch
:
172 upstream_branch
= 'origin/master'
174 # Get the newest common ancestor between HEAD and our upstream branch.
175 upstream_rev
= git('merge-base', 'HEAD', upstream_branch
)
176 return '%s..' % upstream_rev
179 def get_revs_to_push(rev_range
):
181 rev_range
= get_default_rev_range()
182 # Use git show rather than some plumbing command to figure out which revs
183 # are in rev_range because it handles single revs (HEAD^) and ranges
184 # (foo..bar) like we want.
185 revs
= git('show', '--reverse', '--quiet',
186 '--pretty=%h', rev_range
).splitlines()
188 die('Nothing to push: No revs in range %s.' % rev_range
)
192 def clean_svn(svn_repo
):
193 svn(svn_repo
, 'revert', '-R', '.')
195 # Unfortunately it appears there's no svn equivalent for git clean, so we
196 # have to do it ourselves.
197 for line
in svn(svn_repo
, 'status', '--no-ignore').split('\n'):
198 if not line
.startswith('?'):
200 filename
= line
[1:].strip()
201 os
.remove(os
.path
.join(svn_repo
, filename
))
204 def svn_init(svn_root
):
205 if not os
.path
.exists(svn_root
):
206 log('Creating svn staging directory: (%s)' % (svn_root
))
207 os
.makedirs(svn_root
)
208 svn(svn_root
, 'checkout', '--depth=empty',
209 'https://llvm.org/svn/llvm-project/', '.')
210 log("svn staging area ready in '%s'" % svn_root
)
211 if not os
.path
.isdir(svn_root
):
212 die("Can't initialize svn staging dir (%s)" % svn_root
)
215 def fix_eol_style_native(rev
, svn_sr_path
, files
):
216 """Fix line endings before applying patches with Unix endings
218 SVN on Windows will check out files with CRLF for files with the
219 svn:eol-style property set to "native". This breaks `git apply`, which
220 typically works with Unix-line ending patches. Work around the problem here
221 by doing a dos2unix up front for files with svn:eol-style set to "native".
222 SVN will not commit a mass line ending re-doing because it detects the line
223 ending format for files with this property.
225 # Skip files that don't exist in SVN yet.
226 files
= [f
for f
in files
if os
.path
.exists(os
.path
.join(svn_sr_path
, f
))]
227 # Use ignore_errors because 'svn propget' prints errors if the file doesn't
228 # have the named property. There doesn't seem to be a way to suppress that.
229 eol_props
= svn(svn_sr_path
, 'propget', 'svn:eol-style', *files
,
233 # No need to split propget output on ' - ' when we have one file.
234 if eol_props
.strip() in ['native', 'CRLF']:
237 for eol_prop
in eol_props
.split('\n'):
239 eol_prop
= eol_prop
.strip('\r')
242 prop_parts
= eol_prop
.rsplit(' - ', 1)
243 if len(prop_parts
) != 2:
244 eprint("unable to parse svn propget line:")
247 (f
, eol_style
) = prop_parts
248 if eol_style
== 'native':
251 # Reformat all files with native SVN line endings to Unix format. SVN
252 # knows files with native line endings are text files. It will commit
253 # just the diff, and not a mass line ending change.
254 shell(['dos2unix'] + crlf_files
, ignore_errors
=True, cwd
=svn_sr_path
)
256 def split_subrepo(f
):
257 # Given a path, splits it into (subproject, rest-of-path). If the path is
258 # not in a subproject, returns ('', full-path).
260 subproject
, remainder
= split_first_path_component(f
)
262 if subproject
in GIT_TO_SVN_DIR
:
263 return subproject
, remainder
267 def get_all_parent_dirs(name
):
269 head
, tail
= os
.path
.split(name
)
272 head
, tail
= os
.path
.split(head
)
275 def svn_push_one_rev(svn_repo
, rev
, dry_run
):
276 files
= git('diff-tree', '--no-commit-id', '--name-only', '-r',
279 raise RuntimeError('Empty diff for rev %s?' % rev
)
281 # Split files by subrepo
282 subrepo_files
= collections
.defaultdict(list)
284 subrepo
, remainder
= split_subrepo(f
)
285 subrepo_files
[subrepo
].append(remainder
)
287 status
= svn(svn_repo
, 'status', '--no-ignore')
289 die("Can't push git rev %s because svn status is not empty:\n%s" %
292 svn_dirs_to_update
= set()
293 for sr
, files
in iteritems(subrepo_files
):
294 svn_sr_path
= GIT_TO_SVN_DIR
[sr
]
296 svn_dirs_to_update
.add(
297 os
.path
.dirname(os
.path
.join(svn_sr_path
, f
)))
299 # We also need to svn update any parent directories which are not yet present
301 for dir in svn_dirs_to_update
:
302 parent_dirs
.update(get_all_parent_dirs(dir))
303 parent_dirs
= set(dir for dir in parent_dirs
304 if not os
.path
.exists(os
.path
.join(svn_repo
, dir)))
305 svn_dirs_to_update
.update(parent_dirs
)
307 # Sort by length to ensure that the parent directories are passed to svn
308 # before child directories.
309 sorted_dirs_to_update
= sorted(svn_dirs_to_update
, key
=len)
311 # SVN update only in the affected directories.
312 svn(svn_repo
, 'update', '--depth=files', *sorted_dirs_to_update
)
314 for sr
, files
in iteritems(subrepo_files
):
315 svn_sr_path
= os
.path
.join(svn_repo
, GIT_TO_SVN_DIR
[sr
])
317 fix_eol_style_native(rev
, svn_sr_path
, files
)
318 # We use text=False (and pass '--binary') so that we can get an exact
319 # diff that can be passed as-is to 'git apply' without any line ending,
320 # encoding, or other mangling.
321 diff
= git('show', '--binary', rev
, '--',
322 *(os
.path
.join(sr
, f
) for f
in files
),
323 strip
=False, text
=False)
324 # git is the only thing that can handle its own patches...
330 shell(['git', 'apply', prefix_strip
, '-'], cwd
=svn_sr_path
,
331 stdin
=diff
, die_on_failure
=False, text
=False)
332 except RuntimeError as e
:
333 eprint("Patch doesn't apply: maybe you should try `git pull -r` "
337 status_lines
= svn(svn_repo
, 'status', '--no-ignore').split('\n')
339 for l
in (l
for l
in status_lines
if (l
.startswith('?') or
341 svn(svn_repo
, 'add', '--no-ignore', l
[1:].strip())
342 for l
in (l
for l
in status_lines
if l
.startswith('!')):
343 svn(svn_repo
, 'remove', l
[1:].strip())
345 # Now we're ready to commit.
346 commit_msg
= git('show', '--pretty=%B', '--quiet', rev
)
348 log(svn(svn_repo
, 'commit', '-m', commit_msg
, '--force-interactive'))
349 log('Committed %s to svn.' % rev
)
351 log("Would have committed %s to svn, if this weren't a dry run." % rev
)
355 '''Push changes back to SVN: this is extracted from Justin Lebar's script
356 available here: https://github.com/jlebar/llvm-repo-tools/
358 Note: a current limitation is that git does not track file rename, so they
359 will show up in SVN as delete+add.
362 git_root
= git('rev-parse', '--show-toplevel')
363 if not os
.path
.isdir(git_root
):
364 die("Can't find git root dir")
366 # Push from the root of the git repo
369 # We need a staging area for SVN, let's hide it in the .git directory.
370 dot_git_dir
= git('rev-parse', '--git-common-dir')
371 svn_root
= os
.path
.join(dot_git_dir
, 'llvm-upstream-svn')
374 rev_range
= args
.rev_range
375 dry_run
= args
.dry_run
376 revs
= get_revs_to_push(rev_range
)
377 log('Pushing %d commit%s:\n%s' %
378 (len(revs
), 's' if len(revs
) != 1
379 else '', '\n'.join(' ' + git('show', '--oneline', '--quiet', c
)
383 svn_push_one_rev(svn_root
, r
, dry_run
)
386 if __name__
== '__main__':
387 if not program_exists('svn'):
388 die('error: git-llvm needs svn command, but svn is not installed.')
391 p
= argparse
.ArgumentParser(
392 prog
='git llvm', formatter_class
=argparse
.RawDescriptionHelpFormatter
,
394 subcommands
= p
.add_subparsers(title
='subcommands',
395 description
='valid subcommands',
396 help='additional help')
397 verbosity_group
= p
.add_mutually_exclusive_group()
398 verbosity_group
.add_argument('-q', '--quiet', action
='store_true',
399 help='print less information')
400 verbosity_group
.add_argument('-v', '--verbose', action
='store_true',
401 help='print more information')
403 parser_push
= subcommands
.add_parser(
404 'push', description
=cmd_push
.__doc
__,
405 help='push changes back to the LLVM SVN repository')
406 parser_push
.add_argument(
411 help='Do everything other than commit to svn. Leaves junk in the svn '
412 'repo, so probably will not work well if you try to commit more '
414 parser_push
.add_argument(
419 help="revs to push (default: everything not in the branch's "
420 'upstream, or not in origin/master if the branch lacks '
421 'an explicit upstream)')
422 parser_push
.set_defaults(func
=cmd_push
)
423 args
= p
.parse_args(argv
)
424 VERBOSE
= args
.verbose
427 # Dispatch to the right subcommand