#!/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. import getpass import optparse import os import subprocess import tempfile import traceback import urllib import sys import re import trychange def Backquote(cmd, cwd=None): """Like running `cmd` in a shell script.""" return subprocess.Popen(cmd, cwd=cwd, stdout=subprocess.PIPE).communicate()[0].strip() def GetTryServerConfig(): """Returns the dictionary of try server options or None if they cannot be found.""" script_path = 'tools/tryserver/tryserver.py' root_dir = Backquote(['git', 'rev-parse', '--show-cdup']) try: script_file = open(os.path.join(root_dir, script_path)) except IOError: return None locals = {} try: exec(script_file, locals) except Exception, e: return None return locals def GetBranchName(working_dir=None): """Return name of current git branch.""" branch = Backquote(['git', 'symbolic-ref', 'HEAD'], working_dir) if not branch.startswith('refs/heads/'): raise "Couldn't figure out branch name" branch = branch[len('refs/heads/'):] return branch def GetPatchName(working_dir=None): """Construct a name for this patch.""" short_sha = Backquote(['git', 'rev-parse', '--short=4', 'HEAD'], working_dir) return GetBranchName() + '-' + short_sha def GetRietveldIssueNumber(): return Backquote(['git', 'config', 'branch.%s.rietveldissue' % GetBranchName()]) def GetRietveldPatchsetNumber(): return Backquote(['git', 'config', 'branch.%s.rietveldpatchset' % GetBranchName()]) def GetSubRepWorkingDir(sub_rep_path): """Computes the path to the sub repository""" if sub_rep_path: root_dir = os.path.abspath(Backquote(['git', 'rev-parse', '--show-cdup'])) return os.path.join(root_dir, sub_rep_path) return None def GetMungedDiff(branch, prefix, sub_rep_path): """Get the diff we'll send to the try server. We munge paths to match svn. We add the prefix that the try bot is expecting. If sub_rep_path is provided, diff will be calculated in the sub repository. We also return the list of files in this diff, without munged paths.""" # Make the following changes: # - Prepend "src/" (or some other prefix) to paths as svn is expecting # - In the case of added files, replace /dev/null with the path to the file # being added. cwd = GetSubRepWorkingDir(sub_rep_path) output = [] if not branch: # Try to guess the upstream branch. branch = Backquote(['git', 'cl', 'upstream'], cwd) command = ['git', 'diff-tree', '-p'] new_cwd = None if not sub_rep_path: command.extend(['--no-prefix']) else: # Append / sub_rep_path = os.path.join(sub_rep_path, '') # Add the right prefix command.extend(['--src-prefix=' + sub_rep_path]) command.extend(['--dst-prefix=' + sub_rep_path]) command.extend([branch, 'HEAD']) # Run diff tree diff = subprocess.Popen(command, stdout=subprocess.PIPE, cwd=cwd).stdout.readlines() # Replace --- /dev/null with --- for i in range(len(diff)): line = diff[i] if line.startswith('--- /dev/null'): line = '--- %s' % diff[i+1][4:] output.append(line) diff = output # Add root prefix output = [] file_set = set() for line in diff: if line.startswith('--- ') or line.startswith('+++ '): filename = line[4:] line = line[0:4] + os.path.join(prefix, filename) file_set.add(filename.rstrip('\r\n')) output.append(line) munged_diff = ''.join(output) if len(munged_diff.strip()) == 0: raise Exception("Patch was empty, did you give the right remote branch?") return (munged_diff, list(file_set)) def OneRepositoryDiff(diff_file, patch_names, branch, prefix, sub_rep_path): """Computes a diff for one git repository at a given path against a given branch. Writes the diff into diff_file and appends a name to the patch_names list. Returns the list of files in the diff.""" (diff, file_list) = GetMungedDiff(branch, prefix, sub_rep_path) # Write the diff out diff_file.write(diff) # Add patch name to list of patches patch_name = GetPatchName(GetSubRepWorkingDir(sub_rep_path)) patch_names.extend([patch_name]) return file_list def ValidEmail(email): return re.match(r"^[a-zA-Z0-9._%-+]+@[a-zA-Z0-9._%-]+.[a-zA-Z]{2,6}$", email) def GetEmail(): email = Backquote(['git', 'config', 'user.email']) runmsg = "Try: git config user.email " assert ValidEmail(email), "Email '%s' is not valid. %s" % (email, runmsg) return email def TryChange(args, file_list): """Put a patch on the try server.""" trychange.TryChange(args, file_list, False) if __name__ == '__main__': parser = optparse.OptionParser( usage='git try [options] [branch]', description='Upload the current diff of branch...HEAD to the try server.') parser.add_option("-b", "--bot", action="append", help="Force the use of a specific build slave (eg mac, " "win, or linux)") parser.add_option("-c", "--clobber", action="store_true", help="Make the try run use be a clobber build") parser.add_option("-r", "--revision", help="Specify the SVN base revision to use") parser.add_option("--root", default="src", metavar="PATH", help="Specify the root prefix that is prepended to paths " "in the patch") parser.add_option("--dry_run", action="store_true", help="Print the diff but don't send it to the try bots") parser.add_option("--sub_rep", nargs=2, action="append", default=[], metavar="PATH BRANCH", help="Specify a path to a git sub-repository and a branch " "to diff with in order to simultanously try changes " "in multiple git repositories. Option may be " "specified multiple times.") parser.add_option("--webkit", metavar="BRANCH", help="Specify webkit branch. Syntactic sugar for " "--sub_rep third_party/WebKit/ ") (options, args) = parser.parse_args(sys.argv) if options.webkit: options.sub_rep.extend([('third_party/WebKit/', options.webkit)]) branch = None if len(args) > 1: branch = args[1] patch_names = [] # Dump all diffs into one diff file. diff_file = tempfile.NamedTemporaryFile() # Calculate diff for main git repository. file_list = OneRepositoryDiff(diff_file, patch_names, branch, options.root, None) # Calculate diff for each extra git repository. for path_and_branch in options.sub_rep: file_list.extend(OneRepositoryDiff(diff_file, patch_names, path_and_branch[1], options.root, path_and_branch[0])) # Make diff file ready for reading. diff_file.flush() # Concatenate patch names # Prepare args for TryChange email = GetEmail() user = email.partition('@')[0] args = [ '-u', user, '-e', email, '-n', '-'.join(patch_names), '--diff', diff_file.name, ] # Send to try server via HTTP if we can parse the config, otherwise # upload via SVN. config = GetTryServerConfig() if config is not None: sendmsg = "Sending %s using HTTP..." % '-'.join(patch_names) args.extend(['--use_http']) if config['try_server_http_host'] is not None: args.extend(['--host', config['try_server_http_host']]) if config['try_server_http_port'] is not None: args.extend(['--port', config['try_server_http_port']]) else: print "Could not get server config -- if you're within Google, " print "do you have have src-internal checked out?" sendmsg = "Sending %s using SVN..." % '-'.join(patch_names) args.extend([ '--use_svn', '--svn_repo', 'svn://svn.chromium.org/chrome-try/try', ]) if options.bot: for bot in options.bot: args.extend(['--bot', bot]) if options.clobber: args.append('--clobber') if options.revision: args.extend(['-r', options.revision]) if GetRietveldPatchsetNumber(): args.extend([ '--issue', GetRietveldIssueNumber(), '--patchset', GetRietveldPatchsetNumber(), ]) if options.dry_run: print open(diff_file.name, 'r').read() exit(0) print sendmsg TryChange(args, file_list)