llvm.org GIT mirror llvm / 7e34ddb
Add some facilities to work with a git monorepo (experimental setup) Add a new script in llvm/utils/git-svn/. When present in the $PATH, it enables a `git llvm` command. It is providing at this point only the ability to push from the git monorepo: `git llvm push`. It is intended to evolves with more features, for instance I plan on features like `git llvm show r284955` to help working with sequential revision numbers. The push feature is taken from Justin Lebar's script available here: https://github.com/jlebar/llvm-repo-tools/ Differential Revision: https://reviews.llvm.org/D26334 git-svn-id: https://llvm.org/svn/llvm-project/llvm/trunk@286138 91177308-0d34-0410-b5e6-96231b3b80d8 Mehdi Amini 2 years ago
1 changed file(s) with 278 addition(s) and 0 deletion(s). Raw diff Collapse all Expand all
0 #!/usr/bin/env python
1 #
2 # ======- git-llvm - LLVM Git Help Integration ---------*- python -*--========#
3 #
4 # The LLVM Compiler Infrastructure
5 #
6 # This file is distributed under the University of Illinois Open Source
7 # License. See LICENSE.TXT for details.
8 #
9 # ==------------------------------------------------------------------------==#
10
11 """
12 git-llvm integration
13 ====================
14
15 This file provides integration for git.
16 """
17
18 from __future__ import print_function
19 import argparse
20 import collections
21 import contextlib
22 import errno
23 import os
24 import re
25 import subprocess
26 import sys
27 import tempfile
28 import time
29 assert sys.version_info >= (2, 7)
30
31
32 # It's *almost* a straightforward mapping from the monorepo to svn...
33 GIT_TO_SVN_DIR = {
34 d: (d + '/trunk')
35 for d in [
36 'clang-tools-extra',
37 'compiler-rt',
38 'dragonegg',
39 'klee',
40 'libclc',
41 'libcxx',
42 'libcxxabi',
43 'lld',
44 'lldb',
45 'llvm',
46 'polly',
47 ]
48 }
49 GIT_TO_SVN_DIR.update({'clang': 'cfe/trunk'})
50
51 VERBOSE = False
52 QUIET = False
53
54
55 def eprint(*args, **kwargs):
56 print(*args, file=sys.stderr, **kwargs)
57
58
59 def log(*args, **kwargs):
60 if QUIET:
61 return
62 print(*args, **kwargs)
63
64
65 def log_verbose(*args, **kwargs):
66 if not VERBOSE:
67 return
68 print(*args, **kwargs)
69
70
71 def die(msg):
72 eprint(msg)
73 sys.exit(1)
74
75
76 def first_dirname(d):
77 while True:
78 (head, tail) = os.path.split(d)
79 if not head or head == '/':
80 return tail
81 d = head
82
83
84 def shell(cmd, strip=True, cwd=None, stdin=None):
85 log_verbose('Running: %s' % ' '.join(cmd))
86
87 start = time.time()
88 p = subprocess.Popen(cmd, cwd=cwd, stdout=subprocess.PIPE,
89 stderr=subprocess.PIPE, stdin=subprocess.PIPE)
90 stdout, stderr = p.communicate(input=stdin)
91 elapsed = time.time() - start
92
93 log_verbose('Command took %0.1fs' % elapsed)
94
95 if p.returncode == 0:
96 if stderr:
97 eprint('`%s` printed to stderr:' % ' '.join(cmd))
98 eprint(stderr.rstrip())
99 if strip:
100 stdout = stdout.rstrip('\r\n')
101 return stdout
102 eprint('`%s` returned %s' % (' '.join(cmd), p.returncode))
103 if stderr:
104 eprint(stderr.rstrip())
105 sys.exit(2)
106
107
108 def git(*cmd, **kwargs):
109 return shell(['git'] + list(cmd), kwargs.get('strip', True))
110
111
112 def svn(cwd, *cmd, **kwargs):
113 # TODO: Better way to do default arg when we have *cmd?
114 return shell(['svn'] + list(cmd), cwd=cwd, stdin=kwargs.get('stdin', None))
115
116
117 def get_default_rev_range():
118 # Get the branch tracked by the current branch, as set by
119 # git branch --set-upstream-to See http://serverfault.com/a/352236/38694.
120 cur_branch = git('rev-parse', '--symbolic-full-name', 'HEAD')
121 upstream_branch = git('for-each-ref', '--format=%(upstream:short)',
122 cur_branch)
123 if not upstream_branch:
124 upstream_branch = 'origin/master'
125
126 # Get the newest common ancestor between HEAD and our upstream branch.
127 upstream_rev = git('merge-base', 'HEAD', upstream_branch)
128 return '%s..' % upstream_rev
129
130
131 def get_revs_to_push(rev_range):
132 if not rev_range:
133 rev_range = get_default_rev_range()
134 # Use git show rather than some plumbing command to figure out which revs
135 # are in rev_range because it handles single revs (HEAD^) and ranges
136 # (foo..bar) like we want.
137 revs = git('show', '--reverse', '--quiet',
138 '--pretty=%h', rev_range).splitlines()
139 if not revs:
140 die('Nothing to push: No revs in range %s.' % rev_range)
141 return revs
142
143
144 def clean_and_update_svn(svn_repo):
145 svn(svn_repo, 'revert', '-R', '.')
146
147 # Unfortunately it appears there's no svn equivalent for git clean, so we
148 # have to do it ourselves.
149 for line in svn(svn_repo, 'status').split('\n'):
150 if not line.startswith('?'):
151 continue
152 filename = line[1:].strip()
153 os.remove(os.path.join(svn_repo, filename))
154
155 svn(svn_repo, 'update', *list(GIT_TO_SVN_DIR.values()))
156
157
158 def svn_init(svn_root):
159 if not os.path.exists(svn_root):
160 log('Creating svn staging directory: (%s)' % (svn_root))
161 os.makedirs(svn_root)
162 log('This is a one-time initialization, please be patient for a few '
163 ' minutes...')
164 svn(svn_root, 'checkout', '--depth=immediates',
165 'https://llvm.org/svn/llvm-project/', '.')
166 svn(svn_root, 'update', *list(GIT_TO_SVN_DIR.values()))
167 log("svn staging area ready in '%s'" % svn_root)
168 if not os.path.isdir(svn_root):
169 die("Can't initialize svn staging dir (%s)" % svn_root)
170
171
172 def svn_push_one_rev(svn_repo, rev, dry_run):
173 files = git('diff-tree', '--no-commit-id', '--name-only', '-r',
174 rev).split('\n')
175 subrepos = {first_dirname(f) for f in files}
176 if not subrepos:
177 raise RuntimeError('Empty diff for rev %s?' % rev)
178
179 status = svn(svn_repo, 'status')
180 if status:
181 die("Can't push git rev %s because svn status is not empty:\n%s" %
182 (rev, status))
183
184 for sr in subrepos:
185 diff = git('show', '--binary', rev, '--', sr, strip=False)
186 svn_sr_path = os.path.join(svn_repo, GIT_TO_SVN_DIR[sr])
187 # git is the only thing that can handle its own patches...
188 log_verbose('Apply patch: %s' % diff)
189 shell(['git', 'apply', '-p2', '-'], cwd=svn_sr_path, stdin=diff)
190
191 status_lines = svn(svn_repo, 'status').split('\n')
192
193 for l in (l for l in status_lines if l.startswith('?')):
194 svn(svn_repo, 'add', l[1:].strip())
195 for l in (l for l in status_lines if l.startswith('!')):
196 svn(svn_repo, 'remove', l[1:].strip())
197
198 # Now we're ready to commit.
199 commit_msg = git('show', '--pretty=%B', '--quiet', rev)
200 if not dry_run:
201 log(svn(svn_repo, 'commit', '-m', commit_msg))
202 log('Committed %s to svn.' % rev)
203 else:
204 log("Would have committed %s to svn, if this weren't a dry run." % rev)
205
206
207 def cmd_push(args):
208 '''Push changes back to SVN: this is extracted from Justin Lebar's script
209 available here: https://github.com/jlebar/llvm-repo-tools/
210
211 Note: a current limitation is that git does not track file rename, so they
212 will show up in SVN as delete+add.
213 '''
214 # Get the git root
215 git_root = git('rev-parse', '--show-toplevel')
216 if not os.path.isdir(git_root):
217 die("Can't find git root dir")
218
219 # Push from the root of the git repo
220 os.chdir(git_root)
221
222 # We need a staging area for SVN, let's hide it in the .git directory.
223 svn_root = os.path.join(git_root, '.git', 'llvm-upstream-svn')
224 svn_init(svn_root)
225
226 rev_range = args.rev_range
227 dry_run = args.dry_run
228 revs = get_revs_to_push(rev_range)
229 log('Pushing %d commit%s:\n%s' %
230 (len(revs), 's' if len(revs) != 1
231 else '', '\n'.join(' ' + git('show', '--oneline', '--quiet', c)
232 for c in revs)))
233 for r in revs:
234 clean_and_update_svn(svn_root)
235 svn_push_one_rev(svn_root, r, dry_run)
236
237
238 if __name__ == '__main__':
239 argv = sys.argv[1:]
240 p = argparse.ArgumentParser(
241 prog='git llvm', formatter_class=argparse.RawDescriptionHelpFormatter,
242 description=__doc__)
243 subcommands = p.add_subparsers(title='subcommands',
244 description='valid subcommands',
245 help='additional help')
246 verbosity_group = p.add_mutually_exclusive_group()
247 verbosity_group.add_argument('-q', '--quiet', action='store_true',
248 help='print less information')
249 verbosity_group.add_argument('-v', '--verbose', action='store_true',
250 help='print more information')
251
252 parser_push = subcommands.add_parser(
253 'push', description=cmd_push.__doc__,
254 help='push changes back to the LLVM SVN repository')
255 parser_push.add_argument(
256 '-n',
257 '--dry-run',
258 dest='dry_run',
259 action='store_true',
260 help='Do everything other than commit to svn. Leaves junk in the svn '
261 'repo, so probably will not work well if you try to commit more '
262 'than one rev.')
263 parser_push.add_argument(
264 'rev_range',
265 metavar='GIT_REVS',
266 type=str,
267 nargs='?',
268 help="revs to push (default: everything not in the branch's "
269 'upstream, or not in origin/master if the branch lacks '
270 'an explicit upstream)')
271 parser_push.set_defaults(func=cmd_push)
272 args = p.parse_args(argv)
273 VERBOSE = args.verbose
274 QUIET = args.quiet
275
276 # Dispatch to the right subcommand
277 args.func(args)