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

Unified 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 side-by-side diff with in-line comments
Download patch
« no previous file with comments | « depot_tools/revert.bat ('k') | depot_tools/tests/abandon.sh » ('j') | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
Index: depot_tools/revert.py
===================================================================
--- depot_tools/revert.py (revision 0)
+++ depot_tools/revert.py (revision 0)
@@ -0,0 +1,284 @@
+#!/usr/bin/python
+# Copyright (c) 2006-2009 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+#
+# Tool to quickly revert a change.
+
+import exceptions
+import optparse
+import os
+import sys
+import xml
+
+import gcl
+import gclient
+
+class ModifiedFile(exceptions.Exception):
+ pass
+class NoModifiedFile(exceptions.Exception):
+ pass
+class NoBlameList(exceptions.Exception):
+ pass
+class OutsideOfCheckout(exceptions.Exception):
+ pass
+
+
+def getTexts(nodelist):
+ """Return a list of texts in the children of a list of DOM nodes."""
+ rc = []
+ for node in nodelist:
+ if node.nodeType == node.TEXT_NODE:
+ rc.append(node.data)
+ else:
+ rc.extend(getTexts(node.childNodes))
+ return rc
+
+
+def RunShellXML(command, print_output=False, keys=None):
+ output = gcl.RunShell(command, print_output)
+ try:
+ dom = xml.dom.minidom.parseString(output)
+ if not keys:
+ return dom
+ result = {}
+ for key in keys:
+ result[key] = getTexts(dom.getElementsByTagName(key))
+ except xml.parsers.expat.ExpatError:
+ print "Failed to parse output:\n%s" % output
+ raise
+ return result
+
+
+def UniqueFast(list):
+ list = [item for item in set(list)]
+ list.sort()
+ return list
+
+
+def GetRepoBase():
+ """Returns the repository base of the root local checkout."""
+ xml_data = RunShellXML(['svn', 'info', '.', '--xml'], keys=['root', 'url'])
+ root = xml_data['root'][0]
+ url = xml_data['url'][0]
+ if not root or not url:
+ raise exceptions.Exception("I'm confused by your checkout")
+ if not url.startswith(root):
+ raise exceptions.Exception("I'm confused by your checkout", url, root)
+ return url[len(root):] + '/'
+
+
+def Revert(revisions, force=False, commit=True, send_email=True, message=None,
+ reviewers=None):
+ """Reverts many revisions in one change list.
+
+ If force is True, it will override local modifications.
+ If commit is True, a commit is done after the revert.
+ If send_mail is True, a review email is sent.
+ If message is True, it is used as the change description.
+ reviewers overrides the blames email addresses for review email."""
+
+ # Use the oldest revision as the primary revision.
+ changename = "revert%d" % revisions[len(revisions)-1]
+ if not force and os.path.exists(gcl.GetChangelistInfoFile(changename)):
+ print "Error, change %s already exist." % changename
+ return 1
+
+ # Move to the repository root and make the revision numbers sorted in
+ # decreasing order.
+ os.chdir(gcl.GetRepositoryRoot())
+ revisions.sort(reverse=True)
+ revisions_string = ",".join([str(rev) for rev in revisions])
+ revisions_string_rev = ",".join([str(-rev) for rev in revisions])
+
+ repo_base = GetRepoBase()
+ files = []
+ blames = []
+ # Get all the modified files by the revision. We'll use this list to optimize
+ # the svn merge.
+ for revision in revisions:
+ log = RunShellXML(["svn", "log", "-r", str(revision), "-v", "--xml"],
+ keys=['path', 'author'])
+ for file in log['path']:
+ # Remove the /trunk/src/ part. The + 1 is for the last slash.
+ if not file.startswith(repo_base):
+ raise OutsideOfCheckout(file)
+ files.append(file[len(repo_base):])
+ blames.extend(log['author'])
+
+ # On Windows, we need to fix the slashes once they got the url part removed.
+ if sys.platform == 'win32':
+ # On Windows, gcl expect the correct slashes.
+ files = [file.replace('/', os.sep) for file in files]
+
+ # Keep unique.
+ files = UniqueFast(files)
+ blames = UniqueFast(blames)
+ if not reviewers:
+ reviewers = blames
+ else:
+ reviewers = UniqueFast(reviewers)
+
+ # Make sure there's something to revert.
+ if not files:
+ raise NoModifiedFile
+ if not reviewers:
+ raise NoBlameList
+
+ if blames:
+ print "Blaming %s\n" % ",".join(blames)
+ if reviewers != blames:
+ print "Emailing %s\n" % ",".join(reviewers)
+ print "These files were modified in %s:" % revisions_string
+ print "\n".join(files)
+ print ""
+
+ # Make sure these files are unmodified with svn status.
+ status = gcl.RunShell(["svn", "status"] + files)
+ if status:
+ if force:
+ # TODO(maruel): Use the tool to correctly revert '?' files.
+ gcl.RunShell(["svn", "revert"] + files)
+ else:
+ raise ModifiedFile(status)
+ # svn up on each of these files
+ gcl.RunShell(["svn", "up"] + files)
+
+ files_status = {}
+ # Extract the first level subpaths. Subversion seems to degrade
+ # exponentially w.r.t. repository size during merges. Working at the root
+ # directory is too rough for svn due to the repository size.
+ roots = UniqueFast([file.split(os.sep)[0] for file in files])
+ for root in roots:
+ # Is it a subdirectory or a files?
+ is_root_subdir = os.path.isdir(root)
+ need_to_update = False
+ if is_root_subdir:
+ os.chdir(root)
+ file_list = []
+ # List the file directly since it is faster when there is only one file.
+ for file in files:
+ if file.startswith(root):
+ file_list.append(file[len(root)+1:])
+ if len(file_list) > 1:
+ # Listing multiple files is not supported by svn merge.
+ file_list = ['.']
+ need_to_update = True
+ else:
+ # Oops, root was in fact a file in the root directory.
+ file_list = [root]
+ root = "."
+
+ print "Reverting %s in %s/" % (revisions_string, root)
+ if need_to_update:
+ # Make sure '.' revision is high enough otherwise merge will be
+ # unhappy.
+ retcode = gcl.RunShellWithReturnCode(['svn', 'up', '.', '-N'])[1]
+ if retcode:
+ print 'svn up . -N failed in %s/.' % root
+ return retcode
+
+ command = ["svn", "merge", "-c", revisions_string_rev]
+ command.extend(file_list)
+ (output, retcode) = gcl.RunShellWithReturnCode(command, print_output=True)
+ if retcode:
+ print "'%s' failed:" % command
+ return retcode
+
+ # Grab the status
+ lines = output.split('\n')
+ for line in lines:
+ if line.startswith('---'):
+ continue
+ if line.startswith('Skipped'):
+ print ""
+ raise ModifiedFile(line[9:-1])
+ # Update the status.
+ status = line[:5] + ' '
+ file = line[5:]
+ if is_root_subdir:
+ files_status[root + os.sep + file] = status
+ else:
+ files_status[file] = status
+
+ if is_root_subdir:
+ os.chdir('..')
+
+ # Transform files_status from a dictionary to a list of tuple.
+ files_status = [(files_status[file], file) for file in files]
+
+ description = "Reverting %s." % revisions_string
+ if message:
+ description += "\n\n"
+ description += message
+ # Don't use gcl.Change() since it prompts the user for infos.
+ change_info = gcl.ChangeInfo(name=changename, issue='',
+ description=description, files=files_status)
+ change_info.Save()
+
+ upload_args = ['-r', ",".join(reviewers)]
+ if send_email:
+ upload_args.append('--send_mail')
+ if commit:
+ upload_args.append('--no_try')
+ gcl.UploadCL(change_info, upload_args)
+
+ retcode = 0
+ if commit:
+ gcl.Commit(change_info, ['--force'])
+ # TODO(maruel): gclient sync (to leave the local checkout in an usable
+ # state)
+ retcode = gclient.Main(["gclient.py", "sync"])
+ return retcode
+
+
+def Main(argv):
+ usage = (
+"""%prog [options] [revision numbers to revert]
+Revert a set of revisions, send the review to Rietveld, sends a review email
+and optionally commit the revert.""")
+
+ parser = optparse.OptionParser(usage=usage)
+ parser.add_option("-c", "--commit", default=False, action="store_true",
+ help="Commits right away.")
+ parser.add_option("-f", "--force", default=False, action="store_true",
+ help="Forces the local modification even if a file is "
+ "already modified locally.")
+ parser.add_option("-n", "--no_email", default=False, action="store_true",
+ help="Inhibits from sending a review email.")
+ parser.add_option("-m", "--message", default=None,
+ help="Additional change description message.")
+ parser.add_option("-r", "--reviewers", action="append",
+ help="Reviewers to send the email to. By default, the list "
+ "of commiters is used.")
+ options, args = parser.parse_args(argv)
+ revisions = []
+ try:
+ for item in args[1:]:
+ revisions.append(int(item))
+ except ValueError:
+ parser.error("You need to pass revision numbers.")
+ if not revisions:
+ parser.error("You need to pass revision numbers.")
+ retcode = 1
+ try:
+ if not os.path.exists(gcl.GetInfoDir()):
+ os.mkdir(gcl.GetInfoDir())
+ retcode = Revert(revisions, options.force, options.commit,
+ not options.no_email, options.message, options.reviewers)
+ except NoBlameList:
+ print "Error: no one to blame."
+ except NoModifiedFile:
+ print "Error: no files to revert."
+ except ModifiedFile, e:
+ print "You need to revert these files since they were already modified:"
+ print "".join(e.args)
+ print "You can use the --force flag to revert the files."
+ except OutsideOfCheckout, e:
+ print "Your repository doesn't contain ", str(e)
+
+ return retcode
+
+
+if __name__ == "__main__":
+ sys.exit(Main(sys.argv))
Property changes on: depot_tools\revert.py
___________________________________________________________________
Added: svn:executable
+ *
Added: svn:eol-style
+ LF
« 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