OLD | NEW |
---|---|
1 #!/usr/bin/env python | 1 #!/usr/bin/env python |
2 # Copyright (c) 2012 The Native Client Authors. All rights reserved. | 2 # Copyright (c) 2012 The Native Client Authors. All rights reserved. |
3 # Use of this source code is governed by a BSD-style license that can be | 3 # Use of this source code is governed by a BSD-style license that can be |
4 # found in the LICENSE file. | 4 # found in the LICENSE file. |
5 | 5 |
6 """ | 6 """ |
7 This tool helps with updating nacl_revision in Chromium's DEPS file to | 7 This tool helps with updating nacl_revision in Chromium's DEPS file to |
8 the latest revision of NaCl. It creates a Rietveld code review for | 8 the latest revision of NaCl. It creates a Rietveld code review for |
9 the update, listing the new NaCl commits. It kicks off a try job, | 9 the update, listing the new NaCl commits. It kicks off a try job, |
10 using relevant trybots. | 10 using relevant trybots. |
11 | 11 |
12 This tool should be run from a Git checkout of Chromium. | 12 This tool should be run from a Git checkout of Chromium. |
13 """ | 13 """ |
14 | 14 |
15 import re | 15 import re |
16 import optparse | 16 import optparse |
17 import os | 17 import os |
18 import subprocess | 18 import subprocess |
19 import sys | 19 import sys |
20 import time | 20 import time |
21 | 21 |
22 # This dependency can be installed with: | 22 |
23 # apt-get install python-svn | 23 NACL_GIT_ROOT = 'native_client' |
24 import pysvn | |
25 | 24 |
26 | 25 |
27 def ReadFile(filename): | 26 def ReadFile(filename): |
28 fh = open(filename, "r") | 27 fh = open(filename, "r") |
29 try: | 28 try: |
30 return fh.read() | 29 return fh.read() |
31 finally: | 30 finally: |
32 fh.close() | 31 fh.close() |
33 | 32 |
34 | 33 |
35 def WriteFile(filename, data): | 34 def WriteFile(filename, data): |
36 fh = open(filename, "w") | 35 fh = open(filename, "w") |
37 try: | 36 try: |
38 fh.write(data) | 37 fh.write(data) |
39 finally: | 38 finally: |
40 fh.close() | 39 fh.close() |
41 | 40 |
42 | 41 |
43 # The 'svn:' URL is faster than the 'http:' URL but only works if you | |
44 # have SVN committer credentials set up. | |
45 # NACL_SVN = 'http://src.chromium.org/native_client/trunk/src/native_client' | |
46 NACL_SVN = 'svn://svn.chromium.org/native_client/trunk/src/native_client' | |
47 # When querying for the latest revision, use the root URL. Otherwise, | |
48 # if the most recent change touched a branch and not trunk, the query | |
49 # will return an empty list. | |
50 NACL_SVN_ROOT = 'svn://svn.chromium.org/native_client' | |
51 | |
52 | |
53 def MatchKey(data, key): | 42 def MatchKey(data, key): |
54 if key == 'nacl_revision': | 43 if key == 'nacl_revision': |
55 # Pattern for fields in Chromium's DEPS file. | 44 # Pattern for fields in Chromium's DEPS file. |
56 match = re.search("^\s*'%s':\s*'(\S+)',\s*(#.*)?$" % key, data, re.M) | 45 match = re.search("^\s*'%s':\s*'(\S+)',\s*(#.*)?$" % key, data, re.M) |
57 else: | 46 else: |
58 # Pattern for fields in NaCl's DEPS file. | 47 # Pattern for fields in NaCl's DEPS file. |
59 match = re.search('^\s*"%s":\s*"(\S+)",\s*(#.*)?$' % key, data, re.M) | 48 match = re.search('^\s*"%s":\s*"(\S+)",\s*(#.*)?$' % key, data, re.M) |
60 if match is None: | 49 if match is None: |
61 raise Exception('Key %r not found' % key) | 50 raise Exception('Key %r not found' % key) |
62 return match | 51 return match |
63 | 52 |
64 | 53 |
65 def GetDepsField(data, key): | 54 def GetDepsField(data, key): |
66 match = MatchKey(data, key) | 55 match = MatchKey(data, key) |
67 return match.group(1) | 56 return match.group(1) |
68 | 57 |
69 | 58 |
70 def SetDepsField(data, key, value): | 59 def SetDepsField(data, key, value): |
71 match = MatchKey(data, key) | 60 match = MatchKey(data, key) |
72 return ''.join([data[:match.start(1)], | 61 return ''.join([data[:match.start(1)], |
73 value, | 62 value, |
74 data[match.end(1):]]) | 63 data[match.end(1):]]) |
75 | 64 |
76 | 65 |
77 def SetDepsFieldWithComment(data, key, value, comment): | 66 def GetNaClRev(git_dir): |
78 assert key == 'nacl_revision', key | 67 return subprocess.check_output( |
79 match = MatchKey(data, key) | 68 ['git', 'rev-parse', 'origin/master'], cwd=git_dir).strip() |
80 return data[:match.start(1)] + value + "', # " + comment + data[match.end():] | |
81 | 69 |
82 | 70 |
83 def GetLatestRootRev(): | 71 def GetLog(git_dir, rev1, rev2): |
84 rev = pysvn.Revision(pysvn.opt_revision_kind.head) | 72 stdout = subprocess.check_output( |
85 lst = pysvn.Client().log(NACL_SVN_ROOT, revision_start=rev, revision_end=rev, | 73 ['git', 'log', '--pretty=format:%h %ae %s', rev1 + '..' + rev2], |
Mark Seaborn
2015/01/28 16:34:21
Can you add "--reverse", so that the oldest change
| |
86 discover_changed_paths=True) | 74 cwd=git_dir) |
87 assert len(lst) == 1, lst | |
88 return lst[0].revision.number | |
89 | |
90 | |
91 def GetNaClRev(): | |
92 # Find the revision number for the most recent commit to the subdir | |
93 # specified by NACL_SVN. Unfortunately, SVN does not make it easy | |
94 # to query this directly. If the HEAD commit did not change the | |
95 # subdir (e.g. because it changed other branches), "svn log | |
96 # -rHEAD:HEAD <subdir>" yields an empty list. This means we must | |
97 # start with the latest root revision and search backwards until we | |
98 # hit a change to the subdir. | |
99 now = time.time() | |
100 rev_num = GetLatestRootRev() | |
101 while True: | |
102 rev = pysvn.Revision(pysvn.opt_revision_kind.number, rev_num) | |
103 lst = pysvn.Client().log(NACL_SVN, revision_start=rev, revision_end=rev) | |
104 assert len(lst) in (0, 1), lst | |
105 if len(lst) == 1: | |
106 age_mins = (now - lst[0].date) / 60 | |
107 print 'r%i committed %.1f minutes ago' % ( | |
108 lst[0].revision.number, age_mins) | |
109 return lst[0].revision.number | |
110 rev_num -= 1 | |
111 | |
112 | |
113 def GetLog(rev1, rev2): | |
114 # Get info on commits from rev1 (older, exclusive) to rev2 (newer, | |
115 # inclusive). Returns commit info formatted as a string, and a list | |
116 # of author e-mail addresses. | |
117 items = pysvn.Client().log( | |
118 NACL_SVN, | |
119 revision_start=pysvn.Revision(pysvn.opt_revision_kind.number, rev1 + 1), | |
120 revision_end=pysvn.Revision(pysvn.opt_revision_kind.number, rev2)) | |
121 got = [] | 75 got = [] |
122 authors = [] | 76 authors = [] |
123 for item in items: | 77 for line in stdout.splitlines(): |
124 line1 = item.message.split('\n')[0] | 78 h, author, message = line.split(' ', 2) |
125 author = item.author.split('@', 1)[0] | 79 authors.append(author) |
126 if line1 == 'Update .DEPS.git' and author == 'chrome-admin': | 80 got.append('%s (%s) %s\n' % (h, author, message)) |
Mark Seaborn
2015/01/28 16:44:15
Can you add a colon, i.e.
'%s: (%s) %s\n'
to mat
| |
127 # Skip these automated commits. | |
128 continue | |
129 authors.append(item.author) | |
130 got.append('r%i: (%s) %s\n' % (item.revision.number, author, line1)) | |
131 return ''.join(got), authors | 81 return ''.join(got), authors |
132 | 82 |
133 | 83 |
134 def AssertIsGitRev(git_rev): | |
135 assert re.match('[0-9a-f]{40}$', git_rev) is not None, git_rev | |
136 | |
137 | |
138 # Returns the result of "git svn find-rev", which converts Git commit IDs | |
139 # to SVN revision numbers and vice versa. | |
140 def GitSvnFindRev(git_dir, arg): | |
141 stdout = subprocess.check_output(['git', 'svn', 'find-rev', arg], | |
142 cwd=git_dir) | |
143 # git-svn annoyingly sends "Partial-building" log output to stdout rather | |
144 # than stderr, so we have to ignore everything except the result on the | |
145 # last line. | |
146 lines = stdout.strip().split('\n') | |
147 return lines[-1].strip() | |
148 | |
149 | |
150 def GitToSvnRev(git_dir, git_rev): | |
151 AssertIsGitRev(git_rev) | |
152 rev = GitSvnFindRev(git_dir, git_rev) | |
153 return int(rev) | |
154 | |
155 | |
156 def SvnToGitRev(git_dir, svn_rev): | |
157 git_rev = GitSvnFindRev(git_dir, 'r%i' % int(svn_rev)) | |
158 AssertIsGitRev(git_rev) | |
159 return git_rev | |
160 | |
161 | |
162 def RunTrybots(): | 84 def RunTrybots(): |
163 try_cmd = ['git', 'cl', 'try'] | 85 try_cmd = ['git', 'cl', 'try'] |
164 # Run the default trybots. | 86 # Run the default trybots. |
165 subprocess.check_call(try_cmd) | 87 subprocess.check_call(try_cmd) |
166 # Run some non-default trybots. | 88 # Run some non-default trybots. |
167 subprocess.check_call( | 89 subprocess.check_call( |
168 try_cmd + ['-m', 'tryserver.chromium.linux', | 90 try_cmd + ['-m', 'tryserver.chromium.linux', |
169 '-b', 'linux_arm', | 91 '-b', 'linux_arm', |
170 '-b', 'linux_arm_compile', | 92 '-b', 'linux_arm_compile', |
171 '-b', 'linux_rel_precise32', | 93 '-b', 'linux_rel_precise32', |
172 '-b', 'linux_nacl_sdk_build', | 94 '-b', 'linux_nacl_sdk_build', |
173 ]) | 95 ]) |
174 subprocess.check_call( | 96 subprocess.check_call( |
175 try_cmd + ['-m', 'tryserver.chromium.mac', | 97 try_cmd + ['-m', 'tryserver.chromium.mac', |
176 '-b', 'mac_nacl_sdk_build', | 98 '-b', 'mac_nacl_sdk_build', |
177 ]) | 99 ]) |
178 subprocess.check_call( | 100 subprocess.check_call( |
179 try_cmd + ['-m', 'tryserver.chromium.win', | 101 try_cmd + ['-m', 'tryserver.chromium.win', |
180 '-b', 'win_nacl_sdk_build', | 102 '-b', 'win_nacl_sdk_build', |
181 ]) | 103 ]) |
182 | 104 |
183 | 105 |
184 def Main(args): | 106 def Main(args): |
185 parser = optparse.OptionParser('%prog [options]\n\n' + __doc__.strip()) | 107 parser = optparse.OptionParser('%prog [options]\n\n' + __doc__.strip()) |
186 parser.add_option('-r', '--revision', default=None, type='int', | 108 parser.add_option('-r', '--revision', default=None, |
187 help='NaCl SVN revision to use (default is HEAD)') | 109 help='NaCl GIT revision to use (default is HEAD)') |
Mark Seaborn
2015/01/28 16:34:21
Nit: capitalise as "Git"
| |
188 parser.add_option('--no-fetch', action='store_true', default=False, | 110 parser.add_option('--no-fetch', action='store_true', default=False, |
189 help='Do not run "git fetch". This is useful to speed' | 111 help='Do not run "git fetch". This is useful to speed' |
190 ' up rerunning the script if you know that the local Git' | 112 ' up rerunning the script if you know that the local Git' |
191 ' repos are already reasonably up-to-date.') | 113 ' repos are already reasonably up-to-date.') |
192 parser.add_option('--force-branch', action='store_true', default=False, | 114 parser.add_option('--force-branch', action='store_true', default=False, |
193 help='Force overwriting the Git branch. This is useful' | 115 help='Force overwriting the Git branch. This is useful' |
194 ' if a previous run of the script failed after creating' | 116 ' if a previous run of the script failed after creating' |
195 ' a nacl-deps-rN branch.') | 117 ' a nacl-deps-rN branch.') |
196 parser.add_option('-n', '--no-commit', action='store_true', default=False, | 118 parser.add_option('-n', '--no-commit', action='store_true', default=False, |
197 help='Do not run "git commit" (implies --no-upload)') | 119 help='Do not run "git commit" (implies --no-upload)') |
(...skipping 10 matching lines...) Expand all Loading... | |
208 # those should be retrievable via the reflog. This can also lose | 130 # those should be retrievable via the reflog. This can also lose |
209 # changes that have been staged to the index but then undone in the | 131 # changes that have been staged to the index but then undone in the |
210 # working files. | 132 # working files. |
211 proc = subprocess.Popen(['git', 'diff', '--name-only', 'HEAD'], | 133 proc = subprocess.Popen(['git', 'diff', '--name-only', 'HEAD'], |
212 stdout=subprocess.PIPE) | 134 stdout=subprocess.PIPE) |
213 changes = proc.communicate()[0] | 135 changes = proc.communicate()[0] |
214 assert proc.wait() == 0, proc.wait() | 136 assert proc.wait() == 0, proc.wait() |
215 if len(changes) != 0: | 137 if len(changes) != 0: |
216 raise AssertionError('You have uncommitted changes:\n%s' % changes) | 138 raise AssertionError('You have uncommitted changes:\n%s' % changes) |
217 | 139 |
218 svn_rev = options.revision | 140 nacl_git_dir = NACL_GIT_ROOT |
219 if svn_rev is None: | |
220 svn_rev = GetNaClRev() | |
221 | |
222 nacl_git_dir = 'native_client' | |
223 if not options.no_fetch: | 141 if not options.no_fetch: |
224 subprocess.check_call(['git', 'fetch']) | 142 subprocess.check_call(['git', 'fetch']) |
225 subprocess.check_call(['git', 'fetch'], cwd=nacl_git_dir) | 143 subprocess.check_call(['git', 'fetch'], cwd=nacl_git_dir) |
JF
2015/01/28 07:12:17
I'm not sure I understand what the NaCl cwd is exp
Mark Seaborn
2015/01/28 16:34:21
This just assumes it's run from a Chromium checkou
bradn
2015/01/28 17:37:32
Correct.
bradn
2015/01/28 17:37:32
What mark said.
| |
226 | 144 |
227 branch_name = 'nacl-deps-r%s' % svn_rev | 145 new_rev = options.revision |
146 if new_rev is None: | |
147 new_rev = GetNaClRev(nacl_git_dir) | |
148 | |
149 branch_name = 'nacl-deps-%s' % new_rev | |
228 if options.force_branch: | 150 if options.force_branch: |
229 checkout_opt = '-B' | 151 checkout_opt = '-B' |
230 else: | 152 else: |
231 checkout_opt = '-b' | 153 checkout_opt = '-b' |
232 subprocess.check_call(['git', 'checkout', checkout_opt, branch_name, | 154 subprocess.check_call(['git', 'checkout', checkout_opt, branch_name, |
233 'origin/master']) | 155 'origin/master']) |
234 | 156 |
235 deps_data = ReadFile('DEPS') | 157 deps_data = ReadFile('DEPS') |
236 old_rev_git = GetDepsField(deps_data, 'nacl_revision') | 158 old_rev = GetDepsField(deps_data, 'nacl_revision') |
237 | 159 |
238 new_rev_comment = 'from svn revision r%s' % svn_rev | 160 deps_data = SetDepsField(deps_data, 'nacl_revision', new_rev) |
239 new_rev_git = SvnToGitRev(nacl_git_dir, svn_rev) | |
240 deps_data = SetDepsFieldWithComment(deps_data, 'nacl_revision', new_rev_git, | |
241 new_rev_comment) | |
242 | 161 |
243 old_rev = GitToSvnRev(nacl_git_dir, old_rev_git) | 162 msg_logs, authors = GetLog(nacl_git_dir, old_rev, new_rev) |
244 | 163 msg = 'NaCl: Update revision in DEPS, %s -> %s' % (old_rev[:9], new_rev[:9]) |
JF
2015/01/28 07:12:17
Isn't the default short hash 7?
Mark Seaborn
2015/01/28 16:34:21
Yes, JF is right. Let's use 7.
bradn
2015/01/28 17:37:32
Done.
bradn
2015/01/28 17:37:32
Done.
| |
245 msg_logs, authors = GetLog(old_rev, svn_rev) | |
246 msg = 'NaCl: Update revision in DEPS, r%i -> r%i' % (old_rev, svn_rev) | |
247 msg += '\n\nThis pulls in the following Native Client changes:\n\n' | 164 msg += '\n\nThis pulls in the following Native Client changes:\n\n' |
248 msg += msg_logs | 165 msg += msg_logs |
249 msg += '\nBUG=none\nTEST=browser_tests and nacl_integration\n' | 166 msg += '\nBUG=none\nTEST=browser_tests and nacl_integration\n' |
250 msg += 'CQ_EXTRA_TRYBOTS=tryserver.chromium.linux:linux_rel_precise32,linux_ar m_compile,linux_nacl_sdk_build\n' | 167 msg += 'CQ_EXTRA_TRYBOTS=tryserver.chromium.linux:linux_rel_precise32,linux_ar m_compile,linux_nacl_sdk_build\n' |
251 print msg | 168 print msg |
252 cc_list = ', '.join(['native-client-reviews@googlegroups.com'] + | 169 cc_list = ', '.join(['native-client-reviews@googlegroups.com'] + |
253 sorted(set(authors))) | 170 sorted(set(authors))) |
254 print 'CC:', cc_list | 171 print 'CC:', cc_list |
255 | 172 |
256 # Copy revision numbers across from native_client/DEPS. | |
257 # We do this because 'From()' is not supported in Chrome's DEPS. | |
258 proc = subprocess.Popen(['svn', 'cat', '%s/DEPS@%s' % (NACL_SVN, svn_rev)], | |
259 stdout=subprocess.PIPE) | |
260 nacl_deps = proc.communicate()[0] | |
261 assert proc.wait() == 0, proc.wait() | |
262 tools_rev = GetDepsField(nacl_deps, 'tools_rev') | |
263 # Chromium's DEPS file used to contain a single "nacl_tools_revision" | |
264 # field that we would update to match NaCl's "tools_rev". Since Chromium | |
265 # switched to using Git revisions in DEPS, "nacl_tools_revision" has been | |
266 # replaced by multiple fields. We don't currently support updating those | |
267 # fields automatically. Instead, just assert that NaCl's "tools_rev" | |
268 # hasn't changed. | |
269 # TODO(mseaborn): Fix this automatic syncing. Maybe this should wait | |
Mark Seaborn
2015/01/28 16:34:21
I guess we can add back this automatic syncing if
bradn
2015/01/28 17:37:32
Yeah I figure its problematic for the moment.
| |
270 # until NaCl has also switched to using Git revisions in its DEPS file. | |
271 assert tools_rev in ('13077', '13800'), tools_rev | |
272 | |
273 WriteFile('DEPS', deps_data) | 173 WriteFile('DEPS', deps_data) |
274 | 174 |
275 if options.no_commit: | 175 if options.no_commit: |
276 return | 176 return |
277 subprocess.check_call(['git', 'commit', '-a', '-m', msg]) | 177 subprocess.check_call(['git', 'commit', '-a', '-m', msg]) |
278 | 178 |
279 if options.no_upload: | 179 if options.no_upload: |
280 return | 180 return |
281 # Override EDITOR so that "git cl upload" will run non-interactively. | 181 # Override EDITOR so that "git cl upload" will run non-interactively. |
282 environ = os.environ.copy() | 182 environ = os.environ.copy() |
(...skipping 11 matching lines...) Expand all Loading... | |
294 # This CC does not happen by default for DEPS. | 194 # This CC does not happen by default for DEPS. |
295 '--cc', cc_list, | 195 '--cc', cc_list, |
296 ], env=environ) | 196 ], env=environ) |
297 if options.no_try: | 197 if options.no_try: |
298 return | 198 return |
299 RunTrybots() | 199 RunTrybots() |
300 | 200 |
301 | 201 |
302 if __name__ == '__main__': | 202 if __name__ == '__main__': |
303 Main(sys.argv[1:]) | 203 Main(sys.argv[1:]) |
OLD | NEW |