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