123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505 |
- #!/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)
|