OLD | NEW |
(Empty) | |
| 1 #!/usr/bin/python |
| 2 |
| 3 # Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file |
| 4 # for details. All rights reserved. Use of this source code is governed by a |
| 5 # BSD-style license that can be found in the LICENSE file. |
| 6 |
| 7 import optparse |
| 8 import os |
| 9 import platform |
| 10 import subprocess |
| 11 import sys |
| 12 |
| 13 """A script used to revert one or a sequence of consecutive CLs, for svn and |
| 14 git-svn users. |
| 15 """ |
| 16 |
| 17 def parse_args(): |
| 18 parser = optparse.OptionParser() |
| 19 parser.add_option('--revisions', '-r', dest='rev_range', action='store', |
| 20 default=None, help='The revision number(s) of the commits ' |
| 21 'you wish to undo. An individual number, or a range (8-10, ' |
| 22 '8..10, or 8:10).') |
| 23 args, _ = parser.parse_args() |
| 24 revision_range = args.rev_range |
| 25 if revision_range == None: |
| 26 maybe_fail('You must specify at least one revision number to revert.') |
| 27 if revision_range.find('-') > -1 or revision_range.find(':') > -1 or \ |
| 28 revision_range.find('..') > -1: |
| 29 # We have a range of commits to revert. |
| 30 split = revision_range.split('-') |
| 31 if len(split) == 1: |
| 32 split = revision_range.split(':') |
| 33 if len(split) == 1: |
| 34 split = revision_range.split('..') |
| 35 start = int(split[0]) |
| 36 end = int(split[1]) |
| 37 if start > end: |
| 38 temp = start |
| 39 start = end |
| 40 end = temp |
| 41 if start != end: |
| 42 maybe_fail('Warning: Are you sure you want to revert a range of ' |
| 43 'revisions? If you just want to revert one CL, only specify ' |
| 44 'one revision number.', user_input=True) |
| 45 else: |
| 46 start = end = int(revision_range) |
| 47 return start, end |
| 48 |
| 49 def maybe_fail(msg, user_input=False): |
| 50 """Determine if we have encountered a condition upon which our script cannot |
| 51 continue, and abort if so. |
| 52 Args: |
| 53 - msg: The error or user prompt message to print. |
| 54 - user_input: True if we require user confirmation to continue. We assume |
| 55 that the user must enter y to proceed. |
| 56 """ |
| 57 if user_input: |
| 58 force = raw_input(msg + ' (y/N) ') |
| 59 if force != 'y': |
| 60 sys.exit(0) |
| 61 else: |
| 62 print msg |
| 63 sys.exit(1) |
| 64 |
| 65 def has_new_code(is_git): |
| 66 """Tests if there are any newer versions of files on the server. |
| 67 Args: |
| 68 - is_git: True if we are working in a git repository. |
| 69 """ |
| 70 os.chdir(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) |
| 71 if not is_git: |
| 72 results, _ = run_cmd(['svn', 'st']) |
| 73 else: |
| 74 results, _ = run_cmd(['git', 'status']) |
| 75 for line in results.split('\n'): |
| 76 if not is_git and (not line.strip().startswith('?') and line != ''): |
| 77 return True |
| 78 elif is_git and ('Changes to be committed' in line or |
| 79 'Changes not staged for commit:' in line): |
| 80 return True |
| 81 if is_git: |
| 82 p = subprocess.Popen(['git', 'log', '-1'], stdout=subprocess.PIPE) |
| 83 output, _ = p.communicate() |
| 84 if find_git_info(output) == None: |
| 85 return True |
| 86 return False |
| 87 |
| 88 def run_cmd(cmd_list, suppress_output=False, std_in=''): |
| 89 """Run the specified command and print out any output to stdout.""" |
| 90 print ' '.join(cmd_list) |
| 91 p = subprocess.Popen(cmd_list, stdout=subprocess.PIPE, stderr=subprocess.PIPE, |
| 92 stdin=subprocess.PIPE, |
| 93 shell=(platform.system()=='Windows')) |
| 94 output, stderr = p.communicate(std_in) |
| 95 if output and not suppress_output: |
| 96 print output |
| 97 if stderr and not suppress_output: |
| 98 print stderr |
| 99 return (output, stderr) |
| 100 |
| 101 def runs_git(): |
| 102 """Returns True if we're standing in an svn-git repository.""" |
| 103 p = subprocess.Popen(['svn', 'info'], stdout=subprocess.PIPE, |
| 104 stderr=subprocess.PIPE) |
| 105 output, err = p.communicate() |
| 106 if err != None and 'is not a working copy' in err: |
| 107 p = subprocess.Popen(['git', 'status'], stdout=subprocess.PIPE) |
| 108 output, _ = p.communicate() |
| 109 if 'fatal: Not a git repository' in output: |
| 110 maybe_fail('Error: not running git or svn.') |
| 111 else: |
| 112 return True |
| 113 return False |
| 114 |
| 115 def find_git_info(git_log, rev_num=None): |
| 116 """Determine the latest svn revision number if rev_num = None, or find the |
| 117 git commit_id that corresponds to a particular svn revision number. |
| 118 """ |
| 119 for line in git_log.split('\n'): |
| 120 tokens = line.split() |
| 121 if len(tokens) == 2 and tokens[0] == 'commit': |
| 122 current_commit_id = tokens[1] |
| 123 elif len(tokens) > 0 and tokens[0] == 'git-svn-id:': |
| 124 revision_number = int(tokens[1].split('@')[1]) |
| 125 if revision_number == rev_num: |
| 126 return current_commit_id |
| 127 if rev_num == None: |
| 128 return revision_number |
| 129 |
| 130 def revert(start, end, is_git): |
| 131 """Revert the sequence of CLs. |
| 132 Args: |
| 133 - start: The first CL to revert. |
| 134 - end: The last CL to revert. |
| 135 - is_git: True if we are in a git-svn checkout. |
| 136 """ |
| 137 if not is_git: |
| 138 _, err = run_cmd(['svn', 'merge', '-r', '%d:%d' % (end, start-1), '.'], |
| 139 std_in='p') |
| 140 if 'Conflict discovered' in err: |
| 141 maybe_fail('Please fix the above conflicts before submitting. Then create' |
| 142 ' a CL and submit your changes to complete the revert.') |
| 143 |
| 144 else: |
| 145 # If we're running git, we have to use the log feature to find the commit |
| 146 # id(s) that correspond to the particular revision number(s). |
| 147 output, _ = run_cmd(['git', 'log', '-1'], suppress_output=True) |
| 148 current_revision = find_git_info(output) |
| 149 distance = (current_revision-start) + 1 |
| 150 output, _ = run_cmd(['git', 'log', '-%d' % distance], suppress_output=True) |
| 151 reverts = [start] |
| 152 commit_msg = '"Reverting %d"' % start |
| 153 if end != start: |
| 154 reverts = range(start, end + 1) |
| 155 reverts.reverse() |
| 156 commit_msg = '%s-%d"' % (commit_msg[:-1], end) |
| 157 for revert in reverts: |
| 158 git_commit_id = find_git_info(output, revert) |
| 159 if git_commit_id == None: |
| 160 maybe_fail('Error: Revision number not found. Is this earlier than your' |
| 161 ' git checkout history?') |
| 162 _, err = run_cmd(['git', 'revert', '-n', git_commit_id]) |
| 163 if 'error: could not revert' in err or 'unmerged' in err: |
| 164 command_sequence = '' |
| 165 for revert in reverts: |
| 166 git_commit_id = find_git_info(output, revert) |
| 167 command_sequence += 'git revert -n %s\n' % git_commit_id |
| 168 maybe_fail('There are conflicts while reverting. Please resolve these ' |
| 169 'after manually running:\n' + command_sequence + 'and then ' |
| 170 'create a CL and submit to complete the revert.') |
| 171 run_cmd(['git', 'commit', '-m', commit_msg]) |
| 172 |
| 173 def main(): |
| 174 revisions = parse_args() |
| 175 git_user = runs_git() |
| 176 if has_new_code(git_user): |
| 177 if git_user: |
| 178 maybe_fail('Your tree has local modifications! Move to a clean tree and ' |
| 179 'try running this script again.') |
| 180 else: |
| 181 maybe_fail('WARNING: This checkout has local modifications!! This could ' |
| 182 'result in a CL that is not just a revert and/or you could lose your' |
| 183 ' local changes! Are you **SURE** you want to continue? ', |
| 184 user_input=True) |
| 185 if git_user: |
| 186 run_cmd(['git', 'cl', 'rebase']) |
| 187 run_cmd(['gclient', 'sync']) |
| 188 revert(revisions[0], revisions[1], git_user) |
| 189 print ('Now, create a CL and submit! The buildbots and your teammates thank ' |
| 190 'you!') |
| 191 |
| 192 if __name__ == '__main__': |
| 193 main() |
OLD | NEW |