Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(137)

Side by Side Diff: depot_tools/revert.py

Issue 92087: Create the Next Generation of depot_tools. Eh. (Closed) Base URL: svn://chrome-svn.corp.google.com/chrome/trunk/tools/
Patch Set: Created 11 years, 8 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « depot_tools/revert.bat ('k') | depot_tools/tests/abandon.sh » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
Property Changes:
Added: svn:executable
+ *
Added: svn:eol-style
+ LF
OLDNEW
(Empty)
1 #!/usr/bin/python
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
4 # found in the LICENSE file.
5 #
6 # Tool to quickly revert a change.
7
8 import exceptions
9 import optparse
10 import os
11 import sys
12 import xml
13
14 import gcl
15 import gclient
16
17 class ModifiedFile(exceptions.Exception):
18 pass
19 class NoModifiedFile(exceptions.Exception):
20 pass
21 class NoBlameList(exceptions.Exception):
22 pass
23 class OutsideOfCheckout(exceptions.Exception):
24 pass
25
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):
54 list = [item for item in set(list)]
55 list.sort()
56 return list
57
58
59 def GetRepoBase():
60 """Returns the repository base of the root local checkout."""
61 xml_data = RunShellXML(['svn', 'info', '.', '--xml'], keys=['root', 'url'])
62 root = xml_data['root'][0]
63 url = xml_data['url'][0]
64 if not root or not url:
65 raise exceptions.Exception("I'm confused by your checkout")
66 if not url.startswith(root):
67 raise exceptions.Exception("I'm confused by your checkout", url, root)
68 return url[len(root):] + '/'
69
70
71 def Revert(revisions, force=False, commit=True, send_email=True, message=None,
72 reviewers=None):
73 """Reverts many revisions in one change list.
74
75 If force is True, it will override local modifications.
76 If commit is True, a commit is done after the revert.
77 If send_mail is True, a review email is sent.
78 If message is True, it is used as the change description.
79 reviewers overrides the blames email addresses for review email."""
80
81 # Use the oldest revision as the primary revision.
82 changename = "revert%d" % revisions[len(revisions)-1]
83 if not force and os.path.exists(gcl.GetChangelistInfoFile(changename)):
84 print "Error, change %s already exist." % changename
85 return 1
86
87 # Move to the repository root and make the revision numbers sorted in
88 # decreasing order.
89 os.chdir(gcl.GetRepositoryRoot())
90 revisions.sort(reverse=True)
91 revisions_string = ",".join([str(rev) for rev in revisions])
92 revisions_string_rev = ",".join([str(-rev) for rev in revisions])
93
94 repo_base = GetRepoBase()
95 files = []
96 blames = []
97 # Get all the modified files by the revision. We'll use this list to optimize
98 # the svn merge.
99 for revision in revisions:
100 log = RunShellXML(["svn", "log", "-r", str(revision), "-v", "--xml"],
101 keys=['path', 'author'])
102 for file in log['path']:
103 # Remove the /trunk/src/ part. The + 1 is for the last slash.
104 if not file.startswith(repo_base):
105 raise OutsideOfCheckout(file)
106 files.append(file[len(repo_base):])
107 blames.extend(log['author'])
108
109 # On Windows, we need to fix the slashes once they got the url part removed.
110 if sys.platform == 'win32':
111 # On Windows, gcl expect the correct slashes.
112 files = [file.replace('/', os.sep) for file in files]
113
114 # Keep unique.
115 files = UniqueFast(files)
116 blames = UniqueFast(blames)
117 if not reviewers:
118 reviewers = blames
119 else:
120 reviewers = UniqueFast(reviewers)
121
122 # Make sure there's something to revert.
123 if not files:
124 raise NoModifiedFile
125 if not reviewers:
126 raise NoBlameList
127
128 if blames:
129 print "Blaming %s\n" % ",".join(blames)
130 if reviewers != blames:
131 print "Emailing %s\n" % ",".join(reviewers)
132 print "These files were modified in %s:" % revisions_string
133 print "\n".join(files)
134 print ""
135
136 # Make sure these files are unmodified with svn status.
137 status = gcl.RunShell(["svn", "status"] + files)
138 if status:
139 if force:
140 # TODO(maruel): Use the tool to correctly revert '?' files.
141 gcl.RunShell(["svn", "revert"] + files)
142 else:
143 raise ModifiedFile(status)
144 # svn up on each of these files
145 gcl.RunShell(["svn", "up"] + files)
146
147 files_status = {}
148 # Extract the first level subpaths. Subversion seems to degrade
149 # exponentially w.r.t. repository size during merges. Working at the root
150 # directory is too rough for svn due to the repository size.
151 roots = UniqueFast([file.split(os.sep)[0] for file in files])
152 for root in roots:
153 # Is it a subdirectory or a files?
154 is_root_subdir = os.path.isdir(root)
155 need_to_update = False
156 if is_root_subdir:
157 os.chdir(root)
158 file_list = []
159 # List the file directly since it is faster when there is only one file.
160 for file in files:
161 if file.startswith(root):
162 file_list.append(file[len(root)+1:])
163 if len(file_list) > 1:
164 # Listing multiple files is not supported by svn merge.
165 file_list = ['.']
166 need_to_update = True
167 else:
168 # Oops, root was in fact a file in the root directory.
169 file_list = [root]
170 root = "."
171
172 print "Reverting %s in %s/" % (revisions_string, root)
173 if need_to_update:
174 # Make sure '.' revision is high enough otherwise merge will be
175 # unhappy.
176 retcode = gcl.RunShellWithReturnCode(['svn', 'up', '.', '-N'])[1]
177 if retcode:
178 print 'svn up . -N failed in %s/.' % root
179 return retcode
180
181 command = ["svn", "merge", "-c", revisions_string_rev]
182 command.extend(file_list)
183 (output, retcode) = gcl.RunShellWithReturnCode(command, print_output=True)
184 if retcode:
185 print "'%s' failed:" % command
186 return retcode
187
188 # Grab the status
189 lines = output.split('\n')
190 for line in lines:
191 if line.startswith('---'):
192 continue
193 if line.startswith('Skipped'):
194 print ""
195 raise ModifiedFile(line[9:-1])
196 # Update the status.
197 status = line[:5] + ' '
198 file = line[5:]
199 if is_root_subdir:
200 files_status[root + os.sep + file] = status
201 else:
202 files_status[file] = status
203
204 if is_root_subdir:
205 os.chdir('..')
206
207 # Transform files_status from a dictionary to a list of tuple.
208 files_status = [(files_status[file], file) for file in files]
209
210 description = "Reverting %s." % revisions_string
211 if message:
212 description += "\n\n"
213 description += message
214 # Don't use gcl.Change() since it prompts the user for infos.
215 change_info = gcl.ChangeInfo(name=changename, issue='',
216 description=description, files=files_status)
217 change_info.Save()
218
219 upload_args = ['-r', ",".join(reviewers)]
220 if send_email:
221 upload_args.append('--send_mail')
222 if commit:
223 upload_args.append('--no_try')
224 gcl.UploadCL(change_info, upload_args)
225
226 retcode = 0
227 if commit:
228 gcl.Commit(change_info, ['--force'])
229 # TODO(maruel): gclient sync (to leave the local checkout in an usable
230 # state)
231 retcode = gclient.Main(["gclient.py", "sync"])
232 return retcode
233
234
235 def Main(argv):
236 usage = (
237 """%prog [options] [revision numbers to revert]
238 Revert a set of revisions, send the review to Rietveld, sends a review email
239 and optionally commit the revert.""")
240
241 parser = optparse.OptionParser(usage=usage)
242 parser.add_option("-c", "--commit", default=False, action="store_true",
243 help="Commits right away.")
244 parser.add_option("-f", "--force", default=False, action="store_true",
245 help="Forces the local modification even if a file is "
246 "already modified locally.")
247 parser.add_option("-n", "--no_email", default=False, action="store_true",
248 help="Inhibits from sending a review email.")
249 parser.add_option("-m", "--message", default=None,
250 help="Additional change description message.")
251 parser.add_option("-r", "--reviewers", action="append",
252 help="Reviewers to send the email to. By default, the list "
253 "of commiters is used.")
254 options, args = parser.parse_args(argv)
255 revisions = []
256 try:
257 for item in args[1:]:
258 revisions.append(int(item))
259 except ValueError:
260 parser.error("You need to pass revision numbers.")
261 if not revisions:
262 parser.error("You need to pass revision numbers.")
263 retcode = 1
264 try:
265 if not os.path.exists(gcl.GetInfoDir()):
266 os.mkdir(gcl.GetInfoDir())
267 retcode = Revert(revisions, options.force, options.commit,
268 not options.no_email, options.message, options.reviewers)
269 except NoBlameList:
270 print "Error: no one to blame."
271 except NoModifiedFile:
272 print "Error: no files to revert."
273 except ModifiedFile, e:
274 print "You need to revert these files since they were already modified:"
275 print "".join(e.args)
276 print "You can use the --force flag to revert the files."
277 except OutsideOfCheckout, e:
278 print "Your repository doesn't contain ", str(e)
279
280 return retcode
281
282
283 if __name__ == "__main__":
284 sys.exit(Main(sys.argv))
OLDNEW
« no previous file with comments | « depot_tools/revert.bat ('k') | depot_tools/tests/abandon.sh » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698