Index: revert.py |
diff --git a/revert.py b/revert.py |
deleted file mode 100755 |
index dc56c1f9c31a371669509169dc29d66c13efb385..0000000000000000000000000000000000000000 |
--- a/revert.py |
+++ /dev/null |
@@ -1,300 +0,0 @@ |
-#!/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 |
-import gclient_scm |
-import gclient_utils |
- |
-class ModifiedFile(exceptions.Exception): |
- pass |
-class NoModifiedFile(exceptions.Exception): |
- pass |
-class NoBlameList(exceptions.Exception): |
- pass |
-class OutsideOfCheckout(exceptions.Exception): |
- pass |
- |
- |
-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.""" |
- info = gclient_scm.CaptureSVNInfo('.') |
- root = info['Repository Root'] |
- url = info['URL'] |
- 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 CaptureSVNLog(args): |
- command = ['log', '--xml'] |
- if args: |
- command += args |
- output = gclient_scm.CaptureSVN(command) |
- dom = gclient_utils.ParseXML(output) |
- entries = [] |
- if dom: |
- # /log/logentry/ |
- # @revision |
- # author|date |
- # paths/ |
- # path (@kind&@action) |
- for node in dom.getElementsByTagName('logentry'): |
- paths = [] |
- for path in node.getElementsByTagName('path'): |
- item = { |
- 'kind': path.getAttribute('kind'), |
- 'action': path.getAttribute('action'), |
- 'path': path.firstChild.nodeValue, |
- } |
- paths.append(item) |
- entry = { |
- 'revision': int(node.getAttribute('revision')), |
- 'author': gclient_utils.GetNamedNodeText(node, 'author'), |
- 'date': gclient_utils.GetNamedNodeText(node, 'date'), |
- 'paths': paths, |
- } |
- entries.append(entry) |
- return entries |
- |
- |
-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. |
- local_root = gcl.GetRepositoryRoot() |
- os.chdir(local_root) |
- revisions.sort(reverse=True) |
- revisions_string = ",".join([str(rev) for rev in revisions]) |
- revisions_string_rev = ",".join([str(-rev) for rev in revisions]) |
- |
- # Get all the modified files by the revision. We'll use this list to optimize |
- # the svn merge. |
- logs = [] |
- for revision in revisions: |
- logs.extend(CaptureSVNLog(["-r", str(revision), "-v"])) |
- |
- files = [] |
- blames = [] |
- repo_base = GetRepoBase() |
- for log in logs: |
- for file in log['paths']: |
- file_name = file['path'] |
- # Remove the /trunk/src/ part. The + 1 is for the last slash. |
- if not file_name.startswith(repo_base): |
- raise OutsideOfCheckout(file_name) |
- files.append(file_name[len(repo_base):]) |
- blames.append(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 = gclient_scm.scm.SVN.CaptureStatus(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(changename, 0, 0, description, files_status, |
- local_root) |
- change_info.Save() |
- |
- upload_args = ['--no_presubmit', '-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, ['--no_presubmit', '--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.") |
- if len(argv) < 2: |
- parser.print_help() |
- return 1; |
- |
- 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)) |