Create directory for appengine-based tryserver.

Also add the upload utility, trychange_git.py. This change has already been reviewed and approved at https://codereview.chromium.org/23205010/

BUG=276183
TBR=cmp@chromium.org

Review URL: https://codereview.chromium.org/23242010

git-svn-id: svn://svn.chromium.org/chrome/trunk/tools/chromium-jobqueue@218721 0039d316-1c4b-4281-b951-d872f2087c98
diff --git a/trychange_git.py b/trychange_git.py
new file mode 100755
index 0000000..28f708b
--- /dev/null
+++ b/trychange_git.py
@@ -0,0 +1,141 @@
+#!/usr/bin/env python
+# Copyright (c) 2013 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 local git changes to a tryserver.
+
+It pushes the local feature branch to a private ref on googlesource
+and posts a description of the job to an appengine instance, where it will get
+picked up by the buildbot tryserver itself.
+"""
+
+from __future__ import print_function
+
+import json
+import os
+import subprocess
+import sys
+import urllib
+
+
+def DieWithError(msg):
+  """Prints the message to stderr and exits."""
+  print(msg, file=sys.stderr)
+  sys.exit(1)
+
+
+def RunGit(*args, **kwargs):
+  """Runs the given git command with arguments, or dies.
+
+  Passes the given kwargs (e.g. cwd or env) through to subprocess."""
+  cmd = ('git',) + args
+  try:
+    return subprocess.check_output(cmd, **kwargs).strip()
+  except subprocess.CalledProcessError as e:
+    DieWithError('Command "%s" failed.\n%s' % (' '.join(cmd), e))
+
+
+def EnsureInGitRepo():
+  """Quick sanity check to make sure we're in a git repo."""
+  if not RunGit('rev-parse', '--is-inside-work-tree') == 'true':
+    DieWithError('You don\'t appear to be inside a git repository.')
+
+
+def GetCodeReviewSettings():
+  """Reads codereview.settings and returns a dict of settings."""
+  top_dir = RunGit('rev-parse', '--show-toplevel')
+  this_dir = os.getcwd()
+  assert this_dir.startswith(top_dir), (top_dir, this_dir)
+
+  settings_file = os.path.join(this_dir, 'codereview.settings')
+  while not os.path.isfile(settings_file):
+    this_dir = os.path.split(this_dir)[0]
+    if not this_dir.startswith(top_dir):
+      DieWithError('Unable to find codereview.settings in this repo.')
+    settings_file = os.path.join(this_dir, 'codereview.settings')
+
+  settings = {}
+  with open(settings_file, 'r') as f:
+    for line in f.readlines():
+      if line.startswith('#'):
+        continue
+      k, v = line.split(':', 1)
+      settings[k.strip()] = v.strip()
+  return settings
+
+
+def PushBranch():
+  """Pushes the current local branch to a ref in the try repo.
+
+  The try repo is either the remote called 'try', or 'origin' otherwise.
+  The ref is '/refs/try/<username>/<local branch>-<short hash>.
+
+  Returns the ref to which the local branch was pushed."""
+  username = RunGit('config', '--get', 'user.email').split('@', 1)[0]
+  branch = RunGit('symbolic-ref', '--short', '-q', 'HEAD')
+  commit = RunGit('rev-parse', branch)[:8]
+  remotes = RunGit('remote').splitlines()
+  if not all((username, branch, commit, remotes)):
+    DieWithError('Unable to get necessary git configuration.')
+
+  remote = 'try' if 'try' in remotes else 'origin'
+  ref = 'refs/try/%s/%s-%s' % (username, branch, commit)
+
+  RunGit('push', remote, '%s:%s' % (branch, ref))
+  return ref
+
+
+def MakeJob(project, jobname, ref):
+  """Creates a job description blob."""
+  email = RunGit('config', '--get', 'user.email')
+  repository = RunGit('config', '--get', 'remote.origin.url')
+  job = {
+      # Fields for buildbot sourcestamp.
+      'project': project,
+      'repository': repository,
+      'branch': ref,
+      'revision': 'HEAD',
+      # Fields for buildbot builder factory.
+      'buildername': jobname,
+      'recipe': project,
+      # Other useful fields.
+      'blamelist': [email],
+  }
+  return json.dumps(job)
+
+
+def PostJob(server, project, job):
+  """POSTs the job description blob to the tryserver instance."""
+  if not server.startswith('https://'):
+    DieWithError('Server URL must be https.')
+  url = '%s/%s/push' % (server, project)
+  data = urllib.urlencode({'job': job})
+  try:
+    conn = urllib.urlopen(url, data)
+  except IOError as e:
+    DieWithError(e)
+  response = conn.getcode()
+  if response != 200:
+    DieWithError('Failed to POST. Got: %d' % response)
+
+
+def Main(_argv):
+  """Main entry point."""
+  EnsureInGitRepo()
+
+  settings = GetCodeReviewSettings()
+  server = settings.get('TRYSERVER_HTTP_HOST')
+  project = settings.get('TRYSERVER_PROJECT')
+  jobnames = settings.get('TRYSERVER_JOB_NAME')
+  if not all((server, project, jobnames)):
+    DieWithError('Missing configuration in codereview.settings.')
+
+  ref = PushBranch()
+  for jobname in jobnames.split(','):
+    job = MakeJob(project, jobname, ref)
+    PostJob(server, project, job)
+
+
+if __name__ == '__main__':
+  sys.exit(Main(sys.argv))