#!/usr/bin/python # Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file # for details. All rights reserved. Use of this source code is governed by a # BSD-style license that can be found in the LICENSE file. import optparse import os import platform import subprocess import sys """A script used to revert one or a sequence of consecutive CLs, for svn and git-svn users. """ def parse_args(): parser = optparse.OptionParser() parser.add_option('--revisions', '-r', dest='rev_range', action='store', default=None, help='The revision number(s) of the commits ' 'you wish to undo. An individual number, or a range (8-10, ' '8..10, or 8:10).') args, _ = parser.parse_args() revision_range = args.rev_range if revision_range is None: maybe_fail('You must specify at least one revision number to revert.') if revision_range.find('-') > -1 or revision_range.find(':') > -1 or \ revision_range.find('..') > -1: # We have a range of commits to revert. split = revision_range.split('-') if len(split) == 1: split = revision_range.split(':') if len(split) == 1: split = revision_range.split('..') start = int(split[0]) end = int(split[1]) if start > end: temp = start start = end end = temp if start != end: maybe_fail('Warning: Are you sure you want to revert a range of ' 'revisions? If you just want to revert one CL, only specify ' 'one revision number.', user_input=True) else: start = end = int(revision_range) return start, end def maybe_fail(msg, user_input=False): """Determine if we have encountered a condition upon which our script cannot continue, and abort if so. Args: - msg: The error or user prompt message to print. - user_input: True if we require user confirmation to continue. We assume that the user must enter y to proceed. """ if user_input: force = raw_input(msg + ' (y/N) ') if force != 'y': sys.exit(0) else: print msg sys.exit(1) def has_new_code(is_git): """Tests if there are any newer versions of files on the server. Args: - is_git: True if we are working in a git repository. """ os.chdir(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) if not is_git: results, _ = run_cmd(['svn', 'st']) else: results, _ = run_cmd(['git', 'status']) for line in results.split('\n'): if not is_git and (not line.strip().startswith('?') and line != ''): return True elif is_git and ('Changes to be committed' in line or 'Changes not staged for commit:' in line): return True if is_git: p = subprocess.Popen(['git', 'log', '-1'], stdout=subprocess.PIPE, shell=(platform.system()=='Windows')) output, _ = p.communicate() if find_git_info(output) is None: return True return False def run_cmd(cmd_list, suppress_output=False, std_in=''): """Run the specified command and print out any output to stdout.""" print ' '.join(cmd_list) p = subprocess.Popen(cmd_list, stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE, shell=(platform.system()=='Windows')) output, stderr = p.communicate(std_in) if output and not suppress_output: print output if stderr and not suppress_output: print stderr return output, stderr def runs_git(): """Returns True if we're standing in an svn-git repository.""" p = subprocess.Popen(['svn', 'info'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=(platform.system()=='Windows')) output, err = p.communicate() if err is not None and 'is not a working copy' in err: p = subprocess.Popen(['git', 'status'], stdout=subprocess.PIPE, shell=(platform.system()=='Windows')) output, _ = p.communicate() if 'fatal: Not a git repository' in output: maybe_fail('Error: not running git or svn.') else: return True return False def find_git_info(git_log, rev_num=None): """Determine the latest svn revision number if rev_num = None, or find the git commit_id that corresponds to a particular svn revision number. """ for line in git_log.split('\n'): tokens = line.split() if len(tokens) == 2 and tokens[0] == 'commit': current_commit_id = tokens[1] elif len(tokens) > 0 and tokens[0] == 'git-svn-id:': revision_number = int(tokens[1].split('@')[1]) if revision_number == rev_num: return current_commit_id if rev_num is None: return revision_number def revert(start, end, is_git): """Revert the sequence of CLs. Args: - start: The first CL to revert. - end: The last CL to revert. - is_git: True if we are in a git-svn checkout. """ if not is_git: _, err = run_cmd(['svn', 'merge', '-r', '%d:%d' % (end, start-1), '.'], std_in='p') if 'Conflict discovered' in err: maybe_fail('Please fix the above conflicts before submitting. Then create' ' a CL and submit your changes to complete the revert.') else: # If we're running git, we have to use the log feature to find the commit # id(s) that correspond to the particular revision number(s). output, _ = run_cmd(['git', 'log', '-1'], suppress_output=True) current_revision = find_git_info(output) distance = (current_revision-start) + 1 output, _ = run_cmd(['git', 'log', '-%d' % distance], suppress_output=True) reverts = [start] commit_msg = '"Reverting %d"' % start if end != start: reverts = range(start, end + 1) reverts.reverse() commit_msg = '%s-%d"' % (commit_msg[:-1], end) for the_revert in reverts: git_commit_id = find_git_info(output, the_revert) if git_commit_id is None: maybe_fail('Error: Revision number not found. Is this earlier than your' ' git checkout history?') _, err = run_cmd(['git', 'revert', '-n', git_commit_id]) if 'error: could not revert' in err or 'unmerged' in err: command_sequence = '' for a_revert in reverts: git_commit_id = find_git_info(output, a_revert) command_sequence += 'git revert -n %s\n' % git_commit_id maybe_fail('There are conflicts while reverting. Please resolve these ' 'after manually running:\n' + command_sequence + 'and then ' 'create a CL and submit to complete the revert.') run_cmd(['git', 'commit', '-m', commit_msg]) def main(): revisions = parse_args() git_user = runs_git() if has_new_code(git_user): maybe_fail('WARNING: This checkout has local modifications!! This could ' 'result in a CL that is not just a revert and/or you could lose your' ' local changes! Are you **SURE** you want to continue? ', user_input=True) if git_user: run_cmd(['git', 'cl', 'rebase']) run_cmd(['gclient', 'sync']) revert(revisions[0], revisions[1], git_user) print ('Now, create a CL and submit! The buildbots and your teammates thank ' 'you!') if __name__ == '__main__': main()