Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(55)

Side by Side Diff: depot_tools/trychange.py

Issue 92087: Create the Next Generation of depot_tools. Eh. (Closed) Base URL: svn://chrome-svn.corp.google.com/chrome/trunk/tools/
Patch Set: Created 11 years, 8 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « depot_tools/tests/test-lib.sh ('k') | depot_tools/upload.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
Property Changes:
Added: svn:executable
+ *
Added: svn:eol-style
+ LF
OLDNEW
(Empty)
1 #!/usr/bin/python
2 # Copyright (c) 2009 The Chromium Authors. All rights reserved.
3 # Use of this source code is governed by a BSD-style license that can be
4 # found in the LICENSE file.
5 """Client-side script to send a try job to the try server. It communicates to
6 the try server by either writting to a svn repository or by directly connecting
7 to the server by HTTP.
8 """
9
10
11 import datetime
12 import getpass
13 import logging
14 import optparse
15 import os
16 import shutil
17 import sys
18 import tempfile
19 import traceback
20 import urllib
21
22 import gcl
23
24 __version__ = '1.1'
25
26
27 # Constants
28 HELP_STRING = "Sorry, Tryserver is not available."
29 SCRIPT_PATH = os.path.join('tools', 'tryserver', 'tryserver.py')
30 USAGE = r"""%prog [options]
31
32 Client-side script to send a try job to the try server. It communicates to
33 the try server by either writting to a svn repository or by directly connecting
34 to the server by HTTP.
35
36
37 Examples:
38 A git patch off a web site (git inserts a/ and b/) and fix the base dir:
39 %prog --url http://url/to/patch.diff --patchlevel 1 --root src
40
41 Use svn to store the try job, specify an alternate email address and use a
42 premade diff file on the local drive:
43 %prog --email user@example.com
44 --svn_repo svn://svn.chromium.org/chrome-try/try --diff foo.diff
45
46 Running only on a 'mac' slave with revision src@123 and clobber first; specify
47 manually the 3 source files to use for the try job:
48 %prog --bot mac --revision src@123 --clobber -f src/a.cc -f src/a.h
49 -f include/b.h
50
51 """
52
53 class InvalidScript(Exception):
54 def __str__(self):
55 return self.args[0] + '\n' + HELP_STRING
56
57
58 class NoTryServerAccess(Exception):
59 def __str__(self):
60 return self.args[0] + '\n' + HELP_STRING
61
62
63 def PathDifference(root, subpath):
64 """Returns the difference subpath minus root."""
65 if subpath.find(root) != 0:
66 return None
67 # The + 1 is for the trailing / or \.
68 return subpath[len(root) + len(os.sep):]
69
70
71 def GetSourceRoot():
72 """Returns the absolute directory one level up from the repository root."""
73 return os.path.abspath(os.path.join(gcl.GetRepositoryRoot(), '..'))
74
75
76 def ExecuteTryServerScript():
77 """Locates the tryserver script, executes it and returns its dictionary.
78
79 The try server script contains the repository-specific try server commands."""
80 script_locals = {}
81 try:
82 # gcl.GetRepositoryRoot() may throw an exception.
83 script_path = os.path.join(gcl.GetRepositoryRoot(), SCRIPT_PATH)
84 except Exception:
85 return script_locals
86 if os.path.exists(script_path):
87 try:
88 exec(gcl.ReadFile(script_path), script_locals)
89 except Exception, e:
90 # TODO(maruel): Need to specialize the exception trapper.
91 traceback.print_exc()
92 raise InvalidScript('%s is invalid.' % script_path)
93 return script_locals
94
95
96 def EscapeDot(name):
97 return name.replace('.', '-')
98
99
100 def RunCommand(command):
101 output, retcode = gcl.RunShellWithReturnCode(command)
102 if retcode:
103 raise NoTryServerAccess(' '.join(command) + '\nOuput:\n' + output)
104 return output
105
106
107 class SCM(object):
108 """Simplistic base class to implement one function: ProcessOptions."""
109 def __init__(self, options):
110 self.options = options
111
112 def ProcessOptions(self):
113 raise Unimplemented
114
115
116 class SVN(SCM):
117 """Gathers the options and diff for a subversion checkout."""
118 def GenerateDiff(self, files, root):
119 """Returns a string containing the diff for the given file list.
120
121 The files in the list should either be absolute paths or relative to the
122 given root. If no root directory is provided, the repository root will be
123 used.
124 """
125 previous_cwd = os.getcwd()
126 if root is None:
127 os.chdir(gcl.GetRepositoryRoot())
128 else:
129 os.chdir(root)
130
131 diff = []
132 for file in files:
133 # Use svn info output instead of os.path.isdir because the latter fails
134 # when the file is deleted.
135 if gcl.GetSVNFileInfo(file).get("Node Kind") == "directory":
136 continue
137 # If the user specified a custom diff command in their svn config file,
138 # then it'll be used when we do svn diff, which we don't want to happen
139 # since we want the unified diff. Using --diff-cmd=diff doesn't always
140 # work, since they can have another diff executable in their path that
141 # gives different line endings. So we use a bogus temp directory as the
142 # config directory, which gets around these problems.
143 if sys.platform.startswith("win"):
144 parent_dir = tempfile.gettempdir()
145 else:
146 parent_dir = sys.path[0] # tempdir is not secure.
147 bogus_dir = os.path.join(parent_dir, "temp_svn_config")
148 if not os.path.exists(bogus_dir):
149 os.mkdir(bogus_dir)
150 # Grabs the diff data.
151 data = gcl.RunShell(["svn", "diff", "--config-dir", bogus_dir, file])
152
153 # We know the diff will be incorrectly formatted. Fix it.
154 if gcl.IsSVNMoved(file):
155 # The file is "new" in the patch sense. Generate a homebrew diff.
156 # We can't use ReadFile() since it's not using binary mode.
157 file_handle = open(file, 'rb')
158 file_content = file_handle.read()
159 file_handle.close()
160 # Prepend '+ ' to every lines.
161 file_content = ['+ ' + i for i in file_content.splitlines(True)]
162 nb_lines = len(file_content)
163 # We need to use / since patch on unix will fail otherwise.
164 file = file.replace('\\', '/')
165 data = "Index: %s\n" % file
166 data += ("============================================================="
167 "======\n")
168 # Note: Should we use /dev/null instead?
169 data += "--- %s\n" % file
170 data += "+++ %s\n" % file
171 data += "@@ -0,0 +1,%d @@\n" % nb_lines
172 data += ''.join(file_content)
173 diff.append(data)
174 os.chdir(previous_cwd)
175 return "".join(diff)
176
177 def ProcessOptions(self):
178 if not self.options.diff:
179 # Generate the diff with svn and write it to the submit queue path. The
180 # files are relative to the repository root, but we need patches relative
181 # to one level up from there (i.e., 'src'), so adjust both the file
182 # paths and the root of the diff.
183 source_root = GetSourceRoot()
184 prefix = PathDifference(source_root, gcl.GetRepositoryRoot())
185 adjusted_paths = [os.path.join(prefix, x) for x in self.options.files]
186 self.options.diff = self.GenerateDiff(adjusted_paths, root=source_root)
187
188
189 class GIT(SCM):
190 """Gathers the options and diff for a git checkout."""
191 def GenerateDiff(self):
192 """Get the diff we'll send to the try server. We ignore the files list."""
193 branch = upload.RunShell(['git', 'cl', 'upstream']).strip()
194 diff = upload.RunShell(['git', 'diff-tree', '-p', '--no-prefix',
195 branch, 'HEAD']).splitlines(True)
196 for i in range(len(diff)):
197 # In the case of added files, replace /dev/null with the path to the
198 # file being added.
199 if diff[i].startswith('--- /dev/null'):
200 diff[i] = '--- %s' % diff[i+1][4:]
201 return ''.join(diff)
202
203 def GetEmail(self):
204 # TODO: check for errors here?
205 return upload.RunShell(['git', 'config', 'user.email']).strip()
206
207 def GetPatchName(self):
208 """Construct a name for this patch."""
209 # TODO: perhaps include the hash of the current commit, to distinguish
210 # patches?
211 branch = upload.RunShell(['git', 'symbolic-ref', 'HEAD']).strip()
212 if not branch.startswith('refs/heads/'):
213 raise "Couldn't figure out branch name"
214 branch = branch[len('refs/heads/'):]
215 return branch
216
217 def ProcessOptions(self):
218 if not self.options.diff:
219 self.options.diff = self.GenerateDiff()
220 if not self.options.name:
221 self.options.name = self.GetPatchName()
222 if not self.options.email:
223 self.options.email = self.GetEmail()
224
225
226 def _ParseSendChangeOptions(options):
227 """Parse common options passed to _SendChangeHTTP and _SendChangeSVN."""
228 values = {}
229 if options.email:
230 values['email'] = options.email
231 values['user'] = options.user
232 values['name'] = options.name
233 if options.bot:
234 values['bot'] = ','.join(options.bot)
235 if options.revision:
236 values['revision'] = options.revision
237 if options.clobber:
238 values['clobber'] = 'true'
239 if options.tests:
240 values['tests'] = ','.join(options.tests)
241 if options.root:
242 values['root'] = options.root
243 if options.patchlevel:
244 values['patchlevel'] = options.patchlevel
245 if options.issue:
246 values['issue'] = options.issue
247 if options.patchset:
248 values['patchset'] = options.patchset
249 return values
250
251
252 def _SendChangeHTTP(options):
253 """Send a change to the try server using the HTTP protocol."""
254 script_locals = ExecuteTryServerScript()
255
256 if not options.host:
257 options.host = script_locals.get('try_server_http_host', None)
258 if not options.host:
259 raise NoTryServerAccess('Please use the --host option to specify the try '
260 'server host to connect to.')
261 if not options.port:
262 options.port = script_locals.get('try_server_http_port', None)
263 if not options.port:
264 raise NoTryServerAccess('Please use the --port option to specify the try '
265 'server port to connect to.')
266
267 values = _ParseSendChangeOptions(options)
268 values['patch'] = options.diff
269
270 url = 'http://%s:%s/send_try_patch' % (options.host, options.port)
271 proxies = None
272 if options.proxy:
273 if options.proxy.lower() == 'none':
274 # Effectively disable HTTP_PROXY or Internet settings proxy setup.
275 proxies = {}
276 else:
277 proxies = {'http': options.proxy, 'https': options.proxy}
278 try:
279 connection = urllib.urlopen(url, urllib.urlencode(values), proxies=proxies)
280 except IOError, e:
281 # TODO(thestig) this probably isn't quite right.
282 if values.get('bot') and e[2] == 'got a bad status line':
283 raise NoTryServerAccess('%s is unaccessible. Bad --bot argument?' % url)
284 else:
285 raise NoTryServerAccess('%s is unaccessible.' % url)
286 if not connection:
287 raise NoTryServerAccess('%s is unaccessible.' % url)
288 if connection.read() != 'OK':
289 raise NoTryServerAccess('%s is unaccessible.' % url)
290 return options.name
291
292
293 def _SendChangeSVN(options):
294 """Send a change to the try server by committing a diff file on a subversion
295 server."""
296 script_locals = ExecuteTryServerScript()
297 if not options.svn_repo:
298 options.svn_repo = script_locals.get('try_server_svn', None)
299 if not options.svn_repo:
300 raise NoTryServerAccess('Please use the --svn_repo option to specify the'
301 ' try server svn repository to connect to.')
302
303 values = _ParseSendChangeOptions(options)
304 description = ''
305 for (k,v) in values.iteritems():
306 description += "%s=%s\n" % (k,v)
307
308 # Do an empty checkout.
309 temp_dir = tempfile.mkdtemp()
310 temp_file = tempfile.NamedTemporaryFile()
311 temp_file_name = temp_file.name
312 try:
313 RunCommand(['svn', 'checkout', '--depth', 'empty', '--non-interactive',
314 options.svn_repo, temp_dir])
315 # TODO(maruel): Use a subdirectory per user?
316 current_time = str(datetime.datetime.now()).replace(':', '.')
317 file_name = (EscapeDot(options.user) + '.' + EscapeDot(options.name) +
318 '.%s.diff' % current_time)
319 full_path = os.path.join(temp_dir, file_name)
320 full_url = options.svn_repo + '/' + file_name
321 file_found = False
322 try:
323 RunCommand(['svn', 'ls', '--non-interactive', full_url])
324 file_found = True
325 except NoTryServerAccess:
326 pass
327 if file_found:
328 # The file already exists in the repo. Note that commiting a file is a
329 # no-op if the file's content (the diff) is not modified. This is why the
330 # file name contains the date and time.
331 RunCommand(['svn', 'update', '--non-interactive', full_path])
332 file = open(full_path, 'wb')
333 file.write(options.diff)
334 file.close()
335 else:
336 # Add the file to the repo
337 file = open(full_path, 'wb')
338 file.write(options.diff)
339 file.close()
340 RunCommand(["svn", "add", '--non-interactive', full_path])
341 temp_file.write(description)
342 temp_file.flush()
343 RunCommand(["svn", "commit", '--non-interactive', full_path, '--file',
344 temp_file_name])
345 finally:
346 temp_file.close()
347 shutil.rmtree(temp_dir, True)
348 return options.name
349
350
351 def GuessVCS(options):
352 """Helper to guess the version control system.
353
354 NOTE: Very similar to upload.GuessVCS. Doesn't look for hg since we don't
355 support it yet.
356
357 This examines the current directory, guesses which SCM we're using, and
358 returns an instance of the appropriate class. Exit with an error if we can't
359 figure it out.
360
361 Returns:
362 A SCM instance. Exits if the SCM can't be guessed.
363 """
364 # Subversion has a .svn in all working directories.
365 if os.path.isdir('.svn'):
366 logging.info("Guessed VCS = Subversion")
367 return SVN(options)
368
369 # Git has a command to test if you're in a git tree.
370 # Try running it, but don't die if we don't have git installed.
371 try:
372 out, returncode = gcl.RunShellWithReturnCode(["git", "rev-parse",
373 "--is-inside-work-tree"])
374 if returncode == 0:
375 logging.info("Guessed VCS = Git")
376 return GIT(options)
377 except OSError, (errno, message):
378 if errno != 2: # ENOENT -- they don't have git installed.
379 raise
380
381 raise NoTryServerAccess("Could not guess version control system. "
382 "Are you in a working copy directory?")
383
384
385 def TryChange(argv,
386 file_list,
387 swallow_exception,
388 prog=None):
389 # Parse argv
390 parser = optparse.OptionParser(usage=USAGE,
391 version=__version__,
392 prog=prog)
393
394 group = optparse.OptionGroup(parser, "Result and status")
395 group.add_option("-u", "--user", default=getpass.getuser(),
396 help="Owner user name [default: %default]")
397 group.add_option("-e", "--email", default=os.environ.get('EMAIL_ADDRESS'),
398 help="Email address where to send the results. Use the "
399 "EMAIL_ADDRESS environment variable to set the default "
400 "email address [default: %default]")
401 group.add_option("-n", "--name", default='Unnamed',
402 help="Descriptive name of the try job")
403 group.add_option("--issue", type='int',
404 help="Update rietveld issue try job status")
405 group.add_option("--patchset", type='int',
406 help="Update rietveld issue try job status")
407 parser.add_option_group(group)
408
409 group = optparse.OptionGroup(parser, "Try job options")
410 group.add_option("-b", "--bot", action="append",
411 help="Only use specifics build slaves, ex: '--bot win' to "
412 "run the try job only on the 'win' slave; see the try "
413 "server watefall for the slave's name")
414 group.add_option("-r", "--revision",
415 help="Revision to use for the try job; default: the "
416 "revision will be determined by the try server; see "
417 "its waterfall for more info")
418 group.add_option("-c", "--clobber", action="store_true",
419 help="Force a clobber before building; e.g. don't do an "
420 "incremental build")
421 # Override the list of tests to run, use multiple times to list many tests
422 # (or comma separated)
423 group.add_option("-t", "--tests", action="append",
424 help=optparse.SUPPRESS_HELP)
425 parser.add_option_group(group)
426
427 group = optparse.OptionGroup(parser, "Patch to run")
428 group.add_option("-f", "--file", default=file_list, dest="files",
429 metavar="FILE", action="append",
430 help="Use many times to list the files to include in the "
431 "try, relative to the repository root")
432 group.add_option("--diff",
433 help="File containing the diff to try")
434 group.add_option("--url",
435 help="Url where to grab a patch")
436 group.add_option("--root",
437 help="Root to use for the patch; base subdirectory for "
438 "patch created in a subdirectory")
439 group.add_option("--patchlevel", type='int', metavar="LEVEL",
440 help="Used as -pN parameter to patch")
441 parser.add_option_group(group)
442
443 group = optparse.OptionGroup(parser, "Access the try server by HTTP")
444 group.add_option("--use_http", action="store_const", const=_SendChangeHTTP,
445 dest="send_patch", default=_SendChangeHTTP,
446 help="Use HTTP to talk to the try server [default]")
447 group.add_option("--host",
448 help="Host address")
449 group.add_option("--port",
450 help="HTTP port")
451 group.add_option("--proxy",
452 help="HTTP proxy")
453 parser.add_option_group(group)
454
455 group = optparse.OptionGroup(parser, "Access the try server with SVN")
456 group.add_option("--use_svn", action="store_const", const=_SendChangeSVN,
457 dest="send_patch",
458 help="Use SVN to talk to the try server")
459 group.add_option("--svn_repo", metavar="SVN_URL",
460 help="SVN url to use to write the changes in; --use_svn is "
461 "implied when using --svn_repo")
462 parser.add_option_group(group)
463
464 options, args = parser.parse_args(argv)
465 # Switch the default accordingly.
466 if options.svn_repo:
467 options.send_patch = _SendChangeSVN
468
469 if len(args) == 1 and args[0] == 'help':
470 parser.print_help()
471 if (not options.files and (not options.issue and options.patchset) and
472 not options.diff and not options.url):
473 # TODO(maruel): It should just try the modified files showing up in a
474 # svn status.
475 print "Nothing to try, changelist is empty."
476 return
477
478 try:
479 # Convert options.diff into the content of the diff.
480 if options.url:
481 options.diff = urllib.urlopen(options.url).read()
482 elif options.diff:
483 options.diff = gcl.ReadFile(options.diff)
484 # Process the VCS in any case at least to retrieve the email address.
485 try:
486 options.scm = GuessVCS(options)
487 options.scm.ProcessOptions()
488 except NoTryServerAccess, e:
489 # If we got the diff, we don't care.
490 if not options.diff:
491 raise
492
493 # Send the patch.
494 patch_name = options.send_patch(options)
495 print 'Patch \'%s\' sent to try server.' % patch_name
496 if patch_name == 'Unnamed':
497 print "Note: use --name NAME to change the try's name."
498 except (InvalidScript, NoTryServerAccess), e:
499 if swallow_exception:
500 return
501 print e
502
503
504 if __name__ == "__main__":
505 TryChange(None, None, False)
OLDNEW
« no previous file with comments | « depot_tools/tests/test-lib.sh ('k') | depot_tools/upload.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698