| OLD | NEW |
| 1 #!/usr/bin/python | 1 #!/usr/bin/python |
| 2 # Copyright (c) 2006-2009 The Chromium Authors. All rights reserved. | 2 # Copyright (c) 2006-2009 The Chromium Authors. All rights reserved. |
| 3 # Use of this source code is governed by a BSD-style license that can be | 3 # Use of this source code is governed by a BSD-style license that can be |
| 4 # found in the LICENSE file. | 4 # found in the LICENSE file. |
| 5 # | 5 # |
| 6 # Tool to quickly revert a change. | 6 # Tool to quickly revert a change. |
| 7 | 7 |
| 8 import exceptions | 8 import exceptions |
| 9 import optparse | 9 import optparse |
| 10 import os | 10 import os |
| 11 import sys | 11 import sys |
| 12 import xml | 12 import xml |
| 13 | 13 |
| 14 import gcl | 14 import gcl |
| 15 import gclient | 15 import gclient |
| 16 | 16 |
| 17 class ModifiedFile(exceptions.Exception): | 17 class ModifiedFile(exceptions.Exception): |
| 18 pass | 18 pass |
| 19 class NoModifiedFile(exceptions.Exception): | 19 class NoModifiedFile(exceptions.Exception): |
| 20 pass | 20 pass |
| 21 class NoBlameList(exceptions.Exception): | 21 class NoBlameList(exceptions.Exception): |
| 22 pass | 22 pass |
| 23 class OutsideOfCheckout(exceptions.Exception): | 23 class OutsideOfCheckout(exceptions.Exception): |
| 24 pass | 24 pass |
| 25 | 25 |
| 26 | 26 |
| 27 def getTexts(nodelist): | |
| 28 """Return a list of texts in the children of a list of DOM nodes.""" | |
| 29 rc = [] | |
| 30 for node in nodelist: | |
| 31 if node.nodeType == node.TEXT_NODE: | |
| 32 rc.append(node.data) | |
| 33 else: | |
| 34 rc.extend(getTexts(node.childNodes)) | |
| 35 return rc | |
| 36 | |
| 37 | |
| 38 def RunShellXML(command, print_output=False, keys=None): | |
| 39 output = gcl.RunShell(command, print_output) | |
| 40 try: | |
| 41 dom = xml.dom.minidom.parseString(output) | |
| 42 if not keys: | |
| 43 return dom | |
| 44 result = {} | |
| 45 for key in keys: | |
| 46 result[key] = getTexts(dom.getElementsByTagName(key)) | |
| 47 except xml.parsers.expat.ExpatError: | |
| 48 print "Failed to parse output:\n%s" % output | |
| 49 raise | |
| 50 return result | |
| 51 | |
| 52 | |
| 53 def UniqueFast(list): | 27 def UniqueFast(list): |
| 54 list = [item for item in set(list)] | 28 list = [item for item in set(list)] |
| 55 list.sort() | 29 list.sort() |
| 56 return list | 30 return list |
| 57 | 31 |
| 58 | 32 |
| 59 def GetRepoBase(): | 33 def GetRepoBase(): |
| 60 """Returns the repository base of the root local checkout.""" | 34 """Returns the repository base of the root local checkout.""" |
| 61 xml_data = RunShellXML(['svn', 'info', '.', '--xml'], keys=['root', 'url']) | 35 info = gclient.CaptureSVNInfo('.') |
| 62 root = xml_data['root'][0] | 36 root = info['Repository Root'] |
| 63 url = xml_data['url'][0] | 37 url = info['URL'] |
| 64 if not root or not url: | 38 if not root or not url: |
| 65 raise exceptions.Exception("I'm confused by your checkout") | 39 raise exceptions.Exception("I'm confused by your checkout") |
| 66 if not url.startswith(root): | 40 if not url.startswith(root): |
| 67 raise exceptions.Exception("I'm confused by your checkout", url, root) | 41 raise exceptions.Exception("I'm confused by your checkout", url, root) |
| 68 return url[len(root):] + '/' | 42 return url[len(root):] + '/' |
| 69 | 43 |
| 70 | 44 |
| 45 def CaptureSVNLog(args): |
| 46 command = ['log', '--xml'] |
| 47 if args: |
| 48 command += args |
| 49 output = gclient.CaptureSVN(command) |
| 50 dom = gclient.ParseXML(output) |
| 51 entries = [] |
| 52 if dom: |
| 53 # /log/logentry/ |
| 54 # @revision |
| 55 # author|date |
| 56 # paths/ |
| 57 # path (@kind&@action) |
| 58 for node in dom.getElementsByTagName('logentry'): |
| 59 paths = [] |
| 60 for path in node.getElementsByTagName('path'): |
| 61 item = { |
| 62 'kind': path.getAttribute('kind'), |
| 63 'action': path.getAttribute('action'), |
| 64 'path': path.firstChild.nodeValue, |
| 65 } |
| 66 paths.append(item) |
| 67 entry = { |
| 68 'revision': int(node.getAttribute('revision')), |
| 69 'author': gclient.GetNamedNodeText(node, 'author'), |
| 70 'date': gclient.GetNamedNodeText(node, 'date'), |
| 71 'paths': paths, |
| 72 } |
| 73 entries.append(entry) |
| 74 return entries |
| 75 |
| 76 |
| 71 def Revert(revisions, force=False, commit=True, send_email=True, message=None, | 77 def Revert(revisions, force=False, commit=True, send_email=True, message=None, |
| 72 reviewers=None): | 78 reviewers=None): |
| 73 """Reverts many revisions in one change list. | 79 """Reverts many revisions in one change list. |
| 74 | 80 |
| 75 If force is True, it will override local modifications. | 81 If force is True, it will override local modifications. |
| 76 If commit is True, a commit is done after the revert. | 82 If commit is True, a commit is done after the revert. |
| 77 If send_mail is True, a review email is sent. | 83 If send_mail is True, a review email is sent. |
| 78 If message is True, it is used as the change description. | 84 If message is True, it is used as the change description. |
| 79 reviewers overrides the blames email addresses for review email.""" | 85 reviewers overrides the blames email addresses for review email.""" |
| 80 | 86 |
| 81 # Use the oldest revision as the primary revision. | 87 # Use the oldest revision as the primary revision. |
| 82 changename = "revert%d" % revisions[len(revisions)-1] | 88 changename = "revert%d" % revisions[len(revisions)-1] |
| 83 if not force and os.path.exists(gcl.GetChangelistInfoFile(changename)): | 89 if not force and os.path.exists(gcl.GetChangelistInfoFile(changename)): |
| 84 print "Error, change %s already exist." % changename | 90 print "Error, change %s already exist." % changename |
| 85 return 1 | 91 return 1 |
| 86 | 92 |
| 87 # Move to the repository root and make the revision numbers sorted in | 93 # Move to the repository root and make the revision numbers sorted in |
| 88 # decreasing order. | 94 # decreasing order. |
| 89 os.chdir(gcl.GetRepositoryRoot()) | 95 os.chdir(gcl.GetRepositoryRoot()) |
| 90 revisions.sort(reverse=True) | 96 revisions.sort(reverse=True) |
| 91 revisions_string = ",".join([str(rev) for rev in revisions]) | 97 revisions_string = ",".join([str(rev) for rev in revisions]) |
| 92 revisions_string_rev = ",".join([str(-rev) for rev in revisions]) | 98 revisions_string_rev = ",".join([str(-rev) for rev in revisions]) |
| 93 | 99 |
| 94 repo_base = GetRepoBase() | 100 # Get all the modified files by the revision. We'll use this list to optimize |
| 101 # the svn merge. |
| 102 logs = [] |
| 103 for revision in revisions: |
| 104 logs.extend(CaptureSVNLog(["-r", str(revision), "-v"])) |
| 105 |
| 95 files = [] | 106 files = [] |
| 96 blames = [] | 107 blames = [] |
| 97 # Get all the modified files by the revision. We'll use this list to optimize | 108 repo_base = GetRepoBase() |
| 98 # the svn merge. | 109 for log in logs: |
| 99 for revision in revisions: | 110 for file in log['paths']: |
| 100 log = RunShellXML(["svn", "log", "-r", str(revision), "-v", "--xml"], | 111 file_name = file['path'] |
| 101 keys=['path', 'author']) | |
| 102 for file in log['path']: | |
| 103 # Remove the /trunk/src/ part. The + 1 is for the last slash. | 112 # Remove the /trunk/src/ part. The + 1 is for the last slash. |
| 104 if not file.startswith(repo_base): | 113 if not file_name.startswith(repo_base): |
| 105 raise OutsideOfCheckout(file) | 114 raise OutsideOfCheckout(file_name) |
| 106 files.append(file[len(repo_base):]) | 115 files.append(file_name[len(repo_base):]) |
| 107 blames.extend(log['author']) | 116 blames.append(log['author']) |
| 108 | 117 |
| 109 # On Windows, we need to fix the slashes once they got the url part removed. | 118 # On Windows, we need to fix the slashes once they got the url part removed. |
| 110 if sys.platform == 'win32': | 119 if sys.platform == 'win32': |
| 111 # On Windows, gcl expect the correct slashes. | 120 # On Windows, gcl expect the correct slashes. |
| 112 files = [file.replace('/', os.sep) for file in files] | 121 files = [file.replace('/', os.sep) for file in files] |
| 113 | 122 |
| 114 # Keep unique. | 123 # Keep unique. |
| 115 files = UniqueFast(files) | 124 files = UniqueFast(files) |
| 116 blames = UniqueFast(blames) | 125 blames = UniqueFast(blames) |
| 117 if not reviewers: | 126 if not reviewers: |
| (...skipping 53 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 171 | 180 |
| 172 print "Reverting %s in %s/" % (revisions_string, root) | 181 print "Reverting %s in %s/" % (revisions_string, root) |
| 173 if need_to_update: | 182 if need_to_update: |
| 174 # Make sure '.' revision is high enough otherwise merge will be | 183 # Make sure '.' revision is high enough otherwise merge will be |
| 175 # unhappy. | 184 # unhappy. |
| 176 retcode = gcl.RunShellWithReturnCode(['svn', 'up', '.', '-N'])[1] | 185 retcode = gcl.RunShellWithReturnCode(['svn', 'up', '.', '-N'])[1] |
| 177 if retcode: | 186 if retcode: |
| 178 print 'svn up . -N failed in %s/.' % root | 187 print 'svn up . -N failed in %s/.' % root |
| 179 return retcode | 188 return retcode |
| 180 | 189 |
| 181 # TODO(maruel): BUG WITH ONLY ONE FILE. | |
| 182 command = ["svn", "merge", "-c", revisions_string_rev] | 190 command = ["svn", "merge", "-c", revisions_string_rev] |
| 183 command.extend(file_list) | 191 command.extend(file_list) |
| 184 (output, retcode) = gcl.RunShellWithReturnCode(command, print_output=True) | 192 (output, retcode) = gcl.RunShellWithReturnCode(command, print_output=True) |
| 185 if retcode: | 193 if retcode: |
| 186 print "'%s' failed:" % command | 194 print "'%s' failed:" % command |
| 187 return retcode | 195 return retcode |
| 188 | 196 |
| 189 # Grab the status | 197 # Grab the status |
| 190 lines = output.split('\n') | 198 lines = output.split('\n') |
| 191 for line in lines: | 199 for line in lines: |
| (...skipping 84 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 276 print "".join(e.args) | 284 print "".join(e.args) |
| 277 print "You can use the --force flag to revert the files." | 285 print "You can use the --force flag to revert the files." |
| 278 except OutsideOfCheckout, e: | 286 except OutsideOfCheckout, e: |
| 279 print "Your repository doesn't contain ", str(e) | 287 print "Your repository doesn't contain ", str(e) |
| 280 | 288 |
| 281 return retcode | 289 return retcode |
| 282 | 290 |
| 283 | 291 |
| 284 if __name__ == "__main__": | 292 if __name__ == "__main__": |
| 285 sys.exit(Main(sys.argv)) | 293 sys.exit(Main(sys.argv)) |
| OLD | NEW |