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 |