Index: depot_tools/trychange.py |
=================================================================== |
--- depot_tools/trychange.py (revision 0) |
+++ depot_tools/trychange.py (revision 0) |
@@ -0,0 +1,505 @@ |
+#!/usr/bin/python |
+# Copyright (c) 2009 The Chromium Authors. All rights reserved. |
+# Use of this source code is governed by a BSD-style license that can be |
+# found in the LICENSE file. |
+"""Client-side script to send a try job to the try server. It communicates to |
+the try server by either writting to a svn repository or by directly connecting |
+to the server by HTTP. |
+""" |
+ |
+ |
+import datetime |
+import getpass |
+import logging |
+import optparse |
+import os |
+import shutil |
+import sys |
+import tempfile |
+import traceback |
+import urllib |
+ |
+import gcl |
+ |
+__version__ = '1.1' |
+ |
+ |
+# Constants |
+HELP_STRING = "Sorry, Tryserver is not available." |
+SCRIPT_PATH = os.path.join('tools', 'tryserver', 'tryserver.py') |
+USAGE = r"""%prog [options] |
+ |
+Client-side script to send a try job to the try server. It communicates to |
+the try server by either writting to a svn repository or by directly connecting |
+to the server by HTTP. |
+ |
+ |
+Examples: |
+ A git patch off a web site (git inserts a/ and b/) and fix the base dir: |
+ %prog --url http://url/to/patch.diff --patchlevel 1 --root src |
+ |
+ Use svn to store the try job, specify an alternate email address and use a |
+ premade diff file on the local drive: |
+ %prog --email user@example.com |
+ --svn_repo svn://svn.chromium.org/chrome-try/try --diff foo.diff |
+ |
+ Running only on a 'mac' slave with revision src@123 and clobber first; specify |
+ manually the 3 source files to use for the try job: |
+ %prog --bot mac --revision src@123 --clobber -f src/a.cc -f src/a.h |
+ -f include/b.h |
+ |
+""" |
+ |
+class InvalidScript(Exception): |
+ def __str__(self): |
+ return self.args[0] + '\n' + HELP_STRING |
+ |
+ |
+class NoTryServerAccess(Exception): |
+ def __str__(self): |
+ return self.args[0] + '\n' + HELP_STRING |
+ |
+ |
+def PathDifference(root, subpath): |
+ """Returns the difference subpath minus root.""" |
+ if subpath.find(root) != 0: |
+ return None |
+ # The + 1 is for the trailing / or \. |
+ return subpath[len(root) + len(os.sep):] |
+ |
+ |
+def GetSourceRoot(): |
+ """Returns the absolute directory one level up from the repository root.""" |
+ return os.path.abspath(os.path.join(gcl.GetRepositoryRoot(), '..')) |
+ |
+ |
+def ExecuteTryServerScript(): |
+ """Locates the tryserver script, executes it and returns its dictionary. |
+ |
+ The try server script contains the repository-specific try server commands.""" |
+ script_locals = {} |
+ try: |
+ # gcl.GetRepositoryRoot() may throw an exception. |
+ script_path = os.path.join(gcl.GetRepositoryRoot(), SCRIPT_PATH) |
+ except Exception: |
+ return script_locals |
+ if os.path.exists(script_path): |
+ try: |
+ exec(gcl.ReadFile(script_path), script_locals) |
+ except Exception, e: |
+ # TODO(maruel): Need to specialize the exception trapper. |
+ traceback.print_exc() |
+ raise InvalidScript('%s is invalid.' % script_path) |
+ return script_locals |
+ |
+ |
+def EscapeDot(name): |
+ return name.replace('.', '-') |
+ |
+ |
+def RunCommand(command): |
+ output, retcode = gcl.RunShellWithReturnCode(command) |
+ if retcode: |
+ raise NoTryServerAccess(' '.join(command) + '\nOuput:\n' + output) |
+ return output |
+ |
+ |
+class SCM(object): |
+ """Simplistic base class to implement one function: ProcessOptions.""" |
+ def __init__(self, options): |
+ self.options = options |
+ |
+ def ProcessOptions(self): |
+ raise Unimplemented |
+ |
+ |
+class SVN(SCM): |
+ """Gathers the options and diff for a subversion checkout.""" |
+ def GenerateDiff(self, files, root): |
+ """Returns a string containing the diff for the given file list. |
+ |
+ The files in the list should either be absolute paths or relative to the |
+ given root. If no root directory is provided, the repository root will be |
+ used. |
+ """ |
+ previous_cwd = os.getcwd() |
+ if root is None: |
+ os.chdir(gcl.GetRepositoryRoot()) |
+ else: |
+ os.chdir(root) |
+ |
+ diff = [] |
+ for file in files: |
+ # Use svn info output instead of os.path.isdir because the latter fails |
+ # when the file is deleted. |
+ if gcl.GetSVNFileInfo(file).get("Node Kind") == "directory": |
+ continue |
+ # If the user specified a custom diff command in their svn config file, |
+ # then it'll be used when we do svn diff, which we don't want to happen |
+ # since we want the unified diff. Using --diff-cmd=diff doesn't always |
+ # work, since they can have another diff executable in their path that |
+ # gives different line endings. So we use a bogus temp directory as the |
+ # config directory, which gets around these problems. |
+ if sys.platform.startswith("win"): |
+ parent_dir = tempfile.gettempdir() |
+ else: |
+ parent_dir = sys.path[0] # tempdir is not secure. |
+ bogus_dir = os.path.join(parent_dir, "temp_svn_config") |
+ if not os.path.exists(bogus_dir): |
+ os.mkdir(bogus_dir) |
+ # Grabs the diff data. |
+ data = gcl.RunShell(["svn", "diff", "--config-dir", bogus_dir, file]) |
+ |
+ # We know the diff will be incorrectly formatted. Fix it. |
+ if gcl.IsSVNMoved(file): |
+ # The file is "new" in the patch sense. Generate a homebrew diff. |
+ # We can't use ReadFile() since it's not using binary mode. |
+ file_handle = open(file, 'rb') |
+ file_content = file_handle.read() |
+ file_handle.close() |
+ # Prepend '+ ' to every lines. |
+ file_content = ['+ ' + i for i in file_content.splitlines(True)] |
+ nb_lines = len(file_content) |
+ # We need to use / since patch on unix will fail otherwise. |
+ file = file.replace('\\', '/') |
+ data = "Index: %s\n" % file |
+ data += ("=============================================================" |
+ "======\n") |
+ # Note: Should we use /dev/null instead? |
+ data += "--- %s\n" % file |
+ data += "+++ %s\n" % file |
+ data += "@@ -0,0 +1,%d @@\n" % nb_lines |
+ data += ''.join(file_content) |
+ diff.append(data) |
+ os.chdir(previous_cwd) |
+ return "".join(diff) |
+ |
+ def ProcessOptions(self): |
+ if not self.options.diff: |
+ # Generate the diff with svn and write it to the submit queue path. The |
+ # files are relative to the repository root, but we need patches relative |
+ # to one level up from there (i.e., 'src'), so adjust both the file |
+ # paths and the root of the diff. |
+ source_root = GetSourceRoot() |
+ prefix = PathDifference(source_root, gcl.GetRepositoryRoot()) |
+ adjusted_paths = [os.path.join(prefix, x) for x in self.options.files] |
+ self.options.diff = self.GenerateDiff(adjusted_paths, root=source_root) |
+ |
+ |
+class GIT(SCM): |
+ """Gathers the options and diff for a git checkout.""" |
+ def GenerateDiff(self): |
+ """Get the diff we'll send to the try server. We ignore the files list.""" |
+ branch = upload.RunShell(['git', 'cl', 'upstream']).strip() |
+ diff = upload.RunShell(['git', 'diff-tree', '-p', '--no-prefix', |
+ branch, 'HEAD']).splitlines(True) |
+ for i in range(len(diff)): |
+ # In the case of added files, replace /dev/null with the path to the |
+ # file being added. |
+ if diff[i].startswith('--- /dev/null'): |
+ diff[i] = '--- %s' % diff[i+1][4:] |
+ return ''.join(diff) |
+ |
+ def GetEmail(self): |
+ # TODO: check for errors here? |
+ return upload.RunShell(['git', 'config', 'user.email']).strip() |
+ |
+ def GetPatchName(self): |
+ """Construct a name for this patch.""" |
+ # TODO: perhaps include the hash of the current commit, to distinguish |
+ # patches? |
+ branch = upload.RunShell(['git', 'symbolic-ref', 'HEAD']).strip() |
+ if not branch.startswith('refs/heads/'): |
+ raise "Couldn't figure out branch name" |
+ branch = branch[len('refs/heads/'):] |
+ return branch |
+ |
+ def ProcessOptions(self): |
+ if not self.options.diff: |
+ self.options.diff = self.GenerateDiff() |
+ if not self.options.name: |
+ self.options.name = self.GetPatchName() |
+ if not self.options.email: |
+ self.options.email = self.GetEmail() |
+ |
+ |
+def _ParseSendChangeOptions(options): |
+ """Parse common options passed to _SendChangeHTTP and _SendChangeSVN.""" |
+ values = {} |
+ if options.email: |
+ values['email'] = options.email |
+ values['user'] = options.user |
+ values['name'] = options.name |
+ if options.bot: |
+ values['bot'] = ','.join(options.bot) |
+ if options.revision: |
+ values['revision'] = options.revision |
+ if options.clobber: |
+ values['clobber'] = 'true' |
+ if options.tests: |
+ values['tests'] = ','.join(options.tests) |
+ if options.root: |
+ values['root'] = options.root |
+ if options.patchlevel: |
+ values['patchlevel'] = options.patchlevel |
+ if options.issue: |
+ values['issue'] = options.issue |
+ if options.patchset: |
+ values['patchset'] = options.patchset |
+ return values |
+ |
+ |
+def _SendChangeHTTP(options): |
+ """Send a change to the try server using the HTTP protocol.""" |
+ script_locals = ExecuteTryServerScript() |
+ |
+ if not options.host: |
+ options.host = script_locals.get('try_server_http_host', None) |
+ if not options.host: |
+ raise NoTryServerAccess('Please use the --host option to specify the try ' |
+ 'server host to connect to.') |
+ if not options.port: |
+ options.port = script_locals.get('try_server_http_port', None) |
+ if not options.port: |
+ raise NoTryServerAccess('Please use the --port option to specify the try ' |
+ 'server port to connect to.') |
+ |
+ values = _ParseSendChangeOptions(options) |
+ values['patch'] = options.diff |
+ |
+ url = 'http://%s:%s/send_try_patch' % (options.host, options.port) |
+ proxies = None |
+ if options.proxy: |
+ if options.proxy.lower() == 'none': |
+ # Effectively disable HTTP_PROXY or Internet settings proxy setup. |
+ proxies = {} |
+ else: |
+ proxies = {'http': options.proxy, 'https': options.proxy} |
+ try: |
+ connection = urllib.urlopen(url, urllib.urlencode(values), proxies=proxies) |
+ except IOError, e: |
+ # TODO(thestig) this probably isn't quite right. |
+ if values.get('bot') and e[2] == 'got a bad status line': |
+ raise NoTryServerAccess('%s is unaccessible. Bad --bot argument?' % url) |
+ else: |
+ raise NoTryServerAccess('%s is unaccessible.' % url) |
+ if not connection: |
+ raise NoTryServerAccess('%s is unaccessible.' % url) |
+ if connection.read() != 'OK': |
+ raise NoTryServerAccess('%s is unaccessible.' % url) |
+ return options.name |
+ |
+ |
+def _SendChangeSVN(options): |
+ """Send a change to the try server by committing a diff file on a subversion |
+ server.""" |
+ script_locals = ExecuteTryServerScript() |
+ if not options.svn_repo: |
+ options.svn_repo = script_locals.get('try_server_svn', None) |
+ if not options.svn_repo: |
+ raise NoTryServerAccess('Please use the --svn_repo option to specify the' |
+ ' try server svn repository to connect to.') |
+ |
+ values = _ParseSendChangeOptions(options) |
+ description = '' |
+ for (k,v) in values.iteritems(): |
+ description += "%s=%s\n" % (k,v) |
+ |
+ # Do an empty checkout. |
+ temp_dir = tempfile.mkdtemp() |
+ temp_file = tempfile.NamedTemporaryFile() |
+ temp_file_name = temp_file.name |
+ try: |
+ RunCommand(['svn', 'checkout', '--depth', 'empty', '--non-interactive', |
+ options.svn_repo, temp_dir]) |
+ # TODO(maruel): Use a subdirectory per user? |
+ current_time = str(datetime.datetime.now()).replace(':', '.') |
+ file_name = (EscapeDot(options.user) + '.' + EscapeDot(options.name) + |
+ '.%s.diff' % current_time) |
+ full_path = os.path.join(temp_dir, file_name) |
+ full_url = options.svn_repo + '/' + file_name |
+ file_found = False |
+ try: |
+ RunCommand(['svn', 'ls', '--non-interactive', full_url]) |
+ file_found = True |
+ except NoTryServerAccess: |
+ pass |
+ if file_found: |
+ # The file already exists in the repo. Note that commiting a file is a |
+ # no-op if the file's content (the diff) is not modified. This is why the |
+ # file name contains the date and time. |
+ RunCommand(['svn', 'update', '--non-interactive', full_path]) |
+ file = open(full_path, 'wb') |
+ file.write(options.diff) |
+ file.close() |
+ else: |
+ # Add the file to the repo |
+ file = open(full_path, 'wb') |
+ file.write(options.diff) |
+ file.close() |
+ RunCommand(["svn", "add", '--non-interactive', full_path]) |
+ temp_file.write(description) |
+ temp_file.flush() |
+ RunCommand(["svn", "commit", '--non-interactive', full_path, '--file', |
+ temp_file_name]) |
+ finally: |
+ temp_file.close() |
+ shutil.rmtree(temp_dir, True) |
+ return options.name |
+ |
+ |
+def GuessVCS(options): |
+ """Helper to guess the version control system. |
+ |
+ NOTE: Very similar to upload.GuessVCS. Doesn't look for hg since we don't |
+ support it yet. |
+ |
+ This examines the current directory, guesses which SCM we're using, and |
+ returns an instance of the appropriate class. Exit with an error if we can't |
+ figure it out. |
+ |
+ Returns: |
+ A SCM instance. Exits if the SCM can't be guessed. |
+ """ |
+ # Subversion has a .svn in all working directories. |
+ if os.path.isdir('.svn'): |
+ logging.info("Guessed VCS = Subversion") |
+ return SVN(options) |
+ |
+ # Git has a command to test if you're in a git tree. |
+ # Try running it, but don't die if we don't have git installed. |
+ try: |
+ out, returncode = gcl.RunShellWithReturnCode(["git", "rev-parse", |
+ "--is-inside-work-tree"]) |
+ if returncode == 0: |
+ logging.info("Guessed VCS = Git") |
+ return GIT(options) |
+ except OSError, (errno, message): |
+ if errno != 2: # ENOENT -- they don't have git installed. |
+ raise |
+ |
+ raise NoTryServerAccess("Could not guess version control system. " |
+ "Are you in a working copy directory?") |
+ |
+ |
+def TryChange(argv, |
+ file_list, |
+ swallow_exception, |
+ prog=None): |
+ # Parse argv |
+ parser = optparse.OptionParser(usage=USAGE, |
+ version=__version__, |
+ prog=prog) |
+ |
+ group = optparse.OptionGroup(parser, "Result and status") |
+ group.add_option("-u", "--user", default=getpass.getuser(), |
+ help="Owner user name [default: %default]") |
+ group.add_option("-e", "--email", default=os.environ.get('EMAIL_ADDRESS'), |
+ help="Email address where to send the results. Use the " |
+ "EMAIL_ADDRESS environment variable to set the default " |
+ "email address [default: %default]") |
+ group.add_option("-n", "--name", default='Unnamed', |
+ help="Descriptive name of the try job") |
+ group.add_option("--issue", type='int', |
+ help="Update rietveld issue try job status") |
+ group.add_option("--patchset", type='int', |
+ help="Update rietveld issue try job status") |
+ parser.add_option_group(group) |
+ |
+ group = optparse.OptionGroup(parser, "Try job options") |
+ group.add_option("-b", "--bot", action="append", |
+ help="Only use specifics build slaves, ex: '--bot win' to " |
+ "run the try job only on the 'win' slave; see the try " |
+ "server watefall for the slave's name") |
+ group.add_option("-r", "--revision", |
+ help="Revision to use for the try job; default: the " |
+ "revision will be determined by the try server; see " |
+ "its waterfall for more info") |
+ group.add_option("-c", "--clobber", action="store_true", |
+ help="Force a clobber before building; e.g. don't do an " |
+ "incremental build") |
+ # Override the list of tests to run, use multiple times to list many tests |
+ # (or comma separated) |
+ group.add_option("-t", "--tests", action="append", |
+ help=optparse.SUPPRESS_HELP) |
+ parser.add_option_group(group) |
+ |
+ group = optparse.OptionGroup(parser, "Patch to run") |
+ group.add_option("-f", "--file", default=file_list, dest="files", |
+ metavar="FILE", action="append", |
+ help="Use many times to list the files to include in the " |
+ "try, relative to the repository root") |
+ group.add_option("--diff", |
+ help="File containing the diff to try") |
+ group.add_option("--url", |
+ help="Url where to grab a patch") |
+ group.add_option("--root", |
+ help="Root to use for the patch; base subdirectory for " |
+ "patch created in a subdirectory") |
+ group.add_option("--patchlevel", type='int', metavar="LEVEL", |
+ help="Used as -pN parameter to patch") |
+ parser.add_option_group(group) |
+ |
+ group = optparse.OptionGroup(parser, "Access the try server by HTTP") |
+ group.add_option("--use_http", action="store_const", const=_SendChangeHTTP, |
+ dest="send_patch", default=_SendChangeHTTP, |
+ help="Use HTTP to talk to the try server [default]") |
+ group.add_option("--host", |
+ help="Host address") |
+ group.add_option("--port", |
+ help="HTTP port") |
+ group.add_option("--proxy", |
+ help="HTTP proxy") |
+ parser.add_option_group(group) |
+ |
+ group = optparse.OptionGroup(parser, "Access the try server with SVN") |
+ group.add_option("--use_svn", action="store_const", const=_SendChangeSVN, |
+ dest="send_patch", |
+ help="Use SVN to talk to the try server") |
+ group.add_option("--svn_repo", metavar="SVN_URL", |
+ help="SVN url to use to write the changes in; --use_svn is " |
+ "implied when using --svn_repo") |
+ parser.add_option_group(group) |
+ |
+ options, args = parser.parse_args(argv) |
+ # Switch the default accordingly. |
+ if options.svn_repo: |
+ options.send_patch = _SendChangeSVN |
+ |
+ if len(args) == 1 and args[0] == 'help': |
+ parser.print_help() |
+ if (not options.files and (not options.issue and options.patchset) and |
+ not options.diff and not options.url): |
+ # TODO(maruel): It should just try the modified files showing up in a |
+ # svn status. |
+ print "Nothing to try, changelist is empty." |
+ return |
+ |
+ try: |
+ # Convert options.diff into the content of the diff. |
+ if options.url: |
+ options.diff = urllib.urlopen(options.url).read() |
+ elif options.diff: |
+ options.diff = gcl.ReadFile(options.diff) |
+ # Process the VCS in any case at least to retrieve the email address. |
+ try: |
+ options.scm = GuessVCS(options) |
+ options.scm.ProcessOptions() |
+ except NoTryServerAccess, e: |
+ # If we got the diff, we don't care. |
+ if not options.diff: |
+ raise |
+ |
+ # Send the patch. |
+ patch_name = options.send_patch(options) |
+ print 'Patch \'%s\' sent to try server.' % patch_name |
+ if patch_name == 'Unnamed': |
+ print "Note: use --name NAME to change the try's name." |
+ except (InvalidScript, NoTryServerAccess), e: |
+ if swallow_exception: |
+ return |
+ print e |
+ |
+ |
+if __name__ == "__main__": |
+ TryChange(None, None, False) |
Property changes on: depot_tools\trychange.py |
___________________________________________________________________ |
Added: svn:executable |
+ * |
Added: svn:eol-style |
+ LF |