Index: drover.py |
diff --git a/drover.py b/drover.py |
deleted file mode 100755 |
index a73f1d347f8d12a78c5b631d1301355fdd7e23e3..0000000000000000000000000000000000000000 |
--- a/drover.py |
+++ /dev/null |
@@ -1,648 +0,0 @@ |
-#!/usr/bin/env python |
-# Copyright (c) 2012 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. |
- |
-import datetime |
-import optparse |
-import os |
-import re |
-import sys |
-import urlparse |
- |
- |
-import gclient_utils |
-import subprocess2 |
- |
-USAGE = """ |
-WARNING: Please use this tool in an empty directory |
-(or at least one that you don't mind clobbering.) |
- |
-REQUIRES: SVN 1.5+ |
-NOTE: NO NEED TO CHECKOUT ANYTHING IN ADVANCE OF USING THIS TOOL. |
-Valid parameters: |
- |
-[Merge from trunk to branch] |
---merge <revision> --branch <branch_num> |
-Example: %(app)s --merge 12345 --branch 187 |
- |
-[Merge from trunk to local copy] |
---merge <revision> --local |
-Example: %(app)s --merge 12345 --local |
- |
-[Merge from branch to branch] |
---merge <revision> --sbranch <branch_num> --branch <branch_num> |
-Example: %(app)s --merge 12345 --sbranch 248 --branch 249 |
- |
-[Revert from trunk] |
---revert <revision> |
-Example: %(app)s --revert 12345 |
- |
-[Revert from branch] |
---revert <revision> --branch <branch_num> |
-Example: %(app)s --revert 12345 --branch 187 |
-""" |
- |
-export_map_ = None |
-files_info_ = None |
-delete_map_ = None |
-file_pattern_ = r"[ ]+([MADUC])[ ]+/((?:trunk|branches/.*?)/src(.*)/(.*))" |
-depot_tools_dir_ = os.path.dirname(os.path.abspath(__file__)) |
- |
- |
-def runGcl(subcommand): |
- gcl_path = os.path.join(depot_tools_dir_, "gcl") |
- if not os.path.exists(gcl_path): |
- print "WARNING: gcl not found beside drover.py. Using system gcl instead..." |
- gcl_path = 'gcl' |
- |
- command = "%s %s" % (gcl_path, subcommand) |
- return os.system(command) |
- |
-def gclUpload(revision, author): |
- command = ("upload " + str(revision) + |
- " --send_mail --no_presubmit --reviewers=" + author) |
- return runGcl(command) |
- |
-def getSVNInfo(url, revision): |
- info = {} |
- svn_info = subprocess2.capture( |
- ['svn', 'info', '--non-interactive', '%s@%s' % (url, revision)], |
- stderr=subprocess2.VOID).splitlines() |
- for line in svn_info: |
- match = re.search(r"(.*?):(.*)", line) |
- if match: |
- info[match.group(1).strip()] = match.group(2).strip() |
- return info |
- |
-def isSVNDirty(): |
- svn_status = subprocess2.check_output(['svn', 'status']).splitlines() |
- for line in svn_status: |
- match = re.search(r"^[^X?]", line) |
- if match: |
- return True |
- |
- return False |
- |
-def getAuthor(url, revision): |
- info = getSVNInfo(url, revision) |
- if (info.has_key("Last Changed Author")): |
- return info["Last Changed Author"] |
- return None |
- |
-def isSVNFile(url, revision): |
- info = getSVNInfo(url, revision) |
- if (info.has_key("Node Kind")): |
- if (info["Node Kind"] == "file"): |
- return True |
- return False |
- |
-def isSVNDirectory(url, revision): |
- info = getSVNInfo(url, revision) |
- if (info.has_key("Node Kind")): |
- if (info["Node Kind"] == "directory"): |
- return True |
- return False |
- |
-def inCheckoutRoot(path): |
- info = getSVNInfo(path, "HEAD") |
- if (not info.has_key("Repository Root")): |
- return False |
- repo_root = info["Repository Root"] |
- info = getSVNInfo(os.path.dirname(os.path.abspath(path)), "HEAD") |
- if (info.get("Repository Root", None) != repo_root): |
- return True |
- return False |
- |
-def getRevisionLog(url, revision): |
- """Takes an svn url and gets the associated revision.""" |
- svn_log = subprocess2.check_output( |
- ['svn', 'log', url, '-r', str(revision)], |
- universal_newlines=True).splitlines(True) |
- # Don't include the header lines and the trailing "---..." line. |
- return ''.join(svn_log[3:-1]) |
- |
-def getSVNVersionInfo(): |
- """Extract version information from SVN""" |
- svn_info = subprocess2.check_output(['svn', '--version']).splitlines() |
- info = {} |
- for line in svn_info: |
- match = re.search(r"svn, version ((\d+)\.(\d+)\.(\d+))", line) |
- if match: |
- info['version'] = match.group(1) |
- info['major'] = int(match.group(2)) |
- info['minor'] = int(match.group(3)) |
- info['patch'] = int(match.group(4)) |
- return info |
- |
- return None |
- |
-def isMinimumSVNVersion(major, minor, patch=0): |
- """Test for minimum SVN version""" |
- return _isMinimumSVNVersion(getSVNVersionInfo(), major, minor, patch) |
- |
-def _isMinimumSVNVersion(version, major, minor, patch=0): |
- """Test for minimum SVN version, internal method""" |
- if not version: |
- return False |
- |
- if (version['major'] > major): |
- return True |
- elif (version['major'] < major): |
- return False |
- |
- if (version['minor'] > minor): |
- return True |
- elif (version['minor'] < minor): |
- return False |
- |
- if (version['patch'] >= patch): |
- return True |
- else: |
- return False |
- |
-def checkoutRevision(url, revision, branch_url, revert=False, pop=True): |
- files_info = getFileInfo(url, revision) |
- paths = getBestMergePaths2(files_info, revision) |
- export_map = getBestExportPathsMap2(files_info, revision) |
- |
- command = 'svn checkout -N ' + branch_url |
- print command |
- os.system(command) |
- |
- match = re.search(r"^[a-z]+://.*/(.*)", branch_url) |
- |
- if match: |
- os.chdir(match.group(1)) |
- |
- # This line is extremely important due to the way svn behaves in the |
- # set-depths action. If parents aren't handled before children, the child |
- # directories get clobbered and the merge step fails. |
- paths.sort() |
- |
- # Checkout the directories that already exist |
- for path in paths: |
- if (export_map.has_key(path) and not revert): |
- print "Exclude new directory " + path |
- continue |
- subpaths = path.split('/') |
- #In the normal case, where no url override is specified and it's just |
- # chromium source, it's necessary to remove the 'trunk' from the filepath, |
- # since in the checkout we include 'trunk' or 'branch/\d+'. |
- # |
- # However, when a url is specified we want to preserve that because it's |
- # a part of the filepath and necessary for path operations on svn (because |
- # frankly, we are checking out the correct top level, and not hacking it). |
- if pop: |
- subpaths.pop(0) |
- base = '' |
- for subpath in subpaths: |
- base += '/' + subpath |
- # This logic ensures that you don't empty out any directories |
- if not os.path.exists("." + base): |
- command = ('svn update --depth empty ' + "." + base) |
- print command |
- os.system(command) |
- |
- if (revert): |
- files = getAllFilesInRevision(files_info) |
- else: |
- files = getExistingFilesInRevision(files_info) |
- |
- for f in files: |
- # Prevent the tool from clobbering the src directory |
- if (f == ""): |
- continue |
- command = ('svn up ".' + f + '"') |
- print command |
- os.system(command) |
- |
-def mergeRevision(url, revision): |
- paths = getBestMergePaths(url, revision) |
- export_map = getBestExportPathsMap(url, revision) |
- |
- for path in paths: |
- if export_map.has_key(path): |
- continue |
- command = ('svn merge -N -r ' + str(revision-1) + ":" + str(revision) + " ") |
- command += " --ignore-ancestry " |
- command += " -x --ignore-eol-style " |
- command += url + path + "@" + str(revision) + " ." + path |
- |
- print command |
- os.system(command) |
- |
-def exportRevision(url, revision): |
- paths = getBestExportPathsMap(url, revision).keys() |
- paths.sort() |
- |
- for path in paths: |
- command = ('svn export -N ' + url + path + "@" + str(revision) + " ." + |
- path) |
- print command |
- os.system(command) |
- |
- command = 'svn add .' + path |
- print command |
- os.system(command) |
- |
-def deleteRevision(url, revision): |
- paths = getBestDeletePathsMap(url, revision).keys() |
- paths.sort() |
- paths.reverse() |
- |
- for path in paths: |
- command = "svn delete ." + path |
- print command |
- os.system(command) |
- |
-def revertExportRevision(url, revision): |
- paths = getBestExportPathsMap(url, revision).keys() |
- paths.sort() |
- paths.reverse() |
- |
- for path in paths: |
- command = "svn delete ." + path |
- print command |
- os.system(command) |
- |
-def revertRevision(url, revision): |
- command = ('svn merge --ignore-ancestry -c -%d %s .' % (revision, url)) |
- print command |
- os.system(command) |
- |
-def getFileInfo(url, revision): |
- global files_info_ |
- |
- if (files_info_ != None): |
- return files_info_ |
- |
- svn_log = subprocess2.check_output( |
- ['svn', 'log', url, '-r', str(revision), '-v']).splitlines() |
- |
- info = [] |
- for line in svn_log: |
- # A workaround to dump the (from .*) stuff, regex not so friendly in the 2nd |
- # pass... |
- match = re.search(r"(.*) \(from.*\)", line) |
- if match: |
- line = match.group(1) |
- match = re.search(file_pattern_, line) |
- if match: |
- info.append([match.group(1).strip(), match.group(2).strip(), |
- match.group(3).strip(),match.group(4).strip()]) |
- |
- files_info_ = info |
- return info |
- |
-def getBestMergePaths(url, revision): |
- """Takes an svn url and gets the associated revision.""" |
- return getBestMergePaths2(getFileInfo(url, revision), revision) |
- |
-def getBestMergePaths2(files_info, revision): |
- """Takes an svn url and gets the associated revision.""" |
- return list(set([f[2] for f in files_info])) |
- |
-def getBestExportPathsMap(url, revision): |
- return getBestExportPathsMap2(getFileInfo(url, revision), revision) |
- |
-def getBestExportPathsMap2(files_info, revision): |
- """Takes an svn url and gets the associated revision.""" |
- global export_map_ |
- |
- if export_map_: |
- return export_map_ |
- |
- result = {} |
- for file_info in files_info: |
- if (file_info[0] == "A"): |
- if(isSVNDirectory("svn://svn.chromium.org/chrome/" + file_info[1], |
- revision)): |
- result[file_info[2] + "/" + file_info[3]] = "" |
- |
- export_map_ = result |
- return result |
- |
-def getBestDeletePathsMap(url, revision): |
- return getBestDeletePathsMap2(getFileInfo(url, revision), revision) |
- |
-def getBestDeletePathsMap2(files_info, revision): |
- """Takes an svn url and gets the associated revision.""" |
- global delete_map_ |
- |
- if delete_map_: |
- return delete_map_ |
- |
- result = {} |
- for file_info in files_info: |
- if (file_info[0] == "D"): |
- if(isSVNDirectory("svn://svn.chromium.org/chrome/" + file_info[1], |
- revision)): |
- result[file_info[2] + "/" + file_info[3]] = "" |
- |
- delete_map_ = result |
- return result |
- |
- |
-def getExistingFilesInRevision(files_info): |
- """Checks for existing files in the revision. |
- |
- Anything that's A will require special treatment (either a merge or an |
- export + add) |
- """ |
- return ['%s/%s' % (f[2], f[3]) for f in files_info if f[0] != 'A'] |
- |
- |
-def getAllFilesInRevision(files_info): |
- """Checks for existing files in the revision. |
- |
- Anything that's A will require special treatment (either a merge or an |
- export + add) |
- """ |
- return ['%s/%s' % (f[2], f[3]) for f in files_info] |
- |
- |
-def getSVNAuthInfo(folder=None): |
- """Fetches SVN authorization information in the subversion auth folder and |
- returns it as a dictionary of dictionaries.""" |
- if not folder: |
- if sys.platform == 'win32': |
- folder = '%%APPDATA%\\Subversion\\auth' |
- else: |
- folder = '~/.subversion/auth' |
- folder = os.path.expandvars(os.path.expanduser(folder)) |
- svn_simple_folder = os.path.join(folder, 'svn.simple') |
- results = {} |
- try: |
- for auth_file in os.listdir(svn_simple_folder): |
- # Read the SVN auth file, convert it into a dictionary, and store it. |
- results[auth_file] = dict(re.findall(r'K [0-9]+\n(.*)\nV [0-9]+\n(.*)\n', |
- open(os.path.join(svn_simple_folder, auth_file)).read())) |
- except Exception as _: |
- pass |
- return results |
- |
- |
-def getCurrentSVNUsers(url): |
- """Tries to fetch the current SVN in the current checkout by scanning the |
- SVN authorization folder for a match with the current SVN URL.""" |
- netloc = urlparse.urlparse(url)[1] |
- auth_infos = getSVNAuthInfo() |
- results = [] |
- for _, auth_info in auth_infos.iteritems(): |
- if ('svn:realmstring' in auth_info |
- and netloc in auth_info['svn:realmstring']): |
- username = auth_info['username'] |
- results.append(username) |
- if 'google.com' in username: |
- results.append(username.replace('google.com', 'chromium.org')) |
- return results |
- |
- |
-def prompt(question): |
- while True: |
- print question + " [y|n]:", |
- answer = sys.stdin.readline() |
- if answer.lower().startswith('n'): |
- return False |
- elif answer.lower().startswith('y'): |
- return True |
- |
- |
-def text_prompt(question, default): |
- print question + " [" + default + "]:" |
- answer = sys.stdin.readline() |
- if answer.strip() == "": |
- return default |
- return answer |
- |
- |
-def drover(options, args): |
- revision = options.revert or options.merge |
- |
- # Initialize some variables used below. They can be overwritten by |
- # the drover.properties file. |
- BASE_URL = "svn://svn.chromium.org/chrome" |
- REVERT_ALT_URLS = ['svn://svn.chromium.org/blink', |
- 'svn://svn.chromium.org/chrome-internal', |
- 'svn://svn.chromium.org/native_client'] |
- TRUNK_URL = BASE_URL + "/trunk/src" |
- BRANCH_URL = BASE_URL + "/branches/$branch/src" |
- SKIP_CHECK_WORKING = True |
- PROMPT_FOR_AUTHOR = False |
- NO_ALT_URLS = options.no_alt_urls |
- |
- DEFAULT_WORKING = "drover_" + str(revision) |
- if options.branch: |
- DEFAULT_WORKING += ("_" + options.branch) |
- |
- if not isMinimumSVNVersion(1, 5): |
- print "You need to use at least SVN version 1.5.x" |
- return 1 |
- |
- # Override the default properties if there is a drover.properties file. |
- global file_pattern_ |
- if os.path.exists("drover.properties"): |
- print 'Using options from %s' % os.path.join( |
- os.getcwd(), 'drover.properties') |
- FILE_PATTERN = file_pattern_ |
- f = open("drover.properties") |
- exec(f) |
- f.close() |
- if FILE_PATTERN: |
- file_pattern_ = FILE_PATTERN |
- NO_ALT_URLS = True |
- |
- if options.revert and options.branch: |
- print 'Note: --branch is usually not needed for reverts.' |
- url = BRANCH_URL.replace("$branch", options.branch) |
- elif options.merge and options.sbranch: |
- url = BRANCH_URL.replace("$branch", options.sbranch) |
- elif options.revert: |
- url = options.url or BASE_URL |
- file_pattern_ = r"[ ]+([MADUC])[ ]+((/.*)/(.*))" |
- else: |
- url = TRUNK_URL |
- |
- working = options.workdir or DEFAULT_WORKING |
- |
- if options.local: |
- working = os.getcwd() |
- if not inCheckoutRoot(working): |
- print "'%s' appears not to be the root of a working copy" % working |
- return 1 |
- if (isSVNDirty() and not |
- prompt("Working copy contains uncommitted files. Continue?")): |
- return 1 |
- |
- if options.revert and not NO_ALT_URLS and not options.url: |
- for cur_url in [url] + REVERT_ALT_URLS: |
- try: |
- commit_date_str = getSVNInfo( |
- cur_url, options.revert).get('Last Changed Date', 'x').split()[0] |
- commit_date = datetime.datetime.strptime(commit_date_str, '%Y-%m-%d') |
- if (datetime.datetime.now() - commit_date).days < 180: |
- if cur_url != url: |
- print 'Guessing svn repo: %s.' % cur_url, |
- print 'Use --no-alt-urls to disable heuristic.' |
- url = cur_url |
- break |
- except ValueError: |
- pass |
- command = 'svn log ' + url + " -r "+str(revision) + " -v" |
- os.system(command) |
- |
- if not (options.revertbot or prompt("Is this the correct revision?")): |
- return 0 |
- |
- if (os.path.exists(working)) and not options.local: |
- if not (options.revertbot or SKIP_CHECK_WORKING or |
- prompt("Working directory: '%s' already exists, clobber?" % working)): |
- return 0 |
- gclient_utils.rmtree(working) |
- |
- if not options.local: |
- os.makedirs(working) |
- os.chdir(working) |
- |
- if options.merge: |
- action = "Merge" |
- if not options.local: |
- branch_url = BRANCH_URL.replace("$branch", options.branch) |
- # Checkout everything but stuff that got added into a new dir |
- checkoutRevision(url, revision, branch_url) |
- # Merge everything that changed |
- mergeRevision(url, revision) |
- # "Export" files that were added from the source and add them to branch |
- exportRevision(url, revision) |
- # Delete directories that were deleted (file deletes are handled in the |
- # merge). |
- deleteRevision(url, revision) |
- elif options.revert: |
- action = "Revert" |
- pop_em = not options.url |
- checkoutRevision(url, revision, url, True, pop_em) |
- revertRevision(url, revision) |
- revertExportRevision(url, revision) |
- |
- # Check the base url so we actually find the author who made the change |
- if options.auditor: |
- author = options.auditor |
- else: |
- author = getAuthor(url, revision) |
- if not author: |
- author = getAuthor(TRUNK_URL, revision) |
- |
- # Check that the author of the CL is different than the user making |
- # the revert. If they're the same, then we'll want to prompt the user |
- # for a different reviewer to TBR. |
- current_users = getCurrentSVNUsers(BASE_URL) |
- is_self_revert = options.revert and author in current_users |
- |
- filename = str(revision)+".txt" |
- out = open(filename,"w") |
- drover_title = '%s %s' % (action, revision) |
- revision_log = getRevisionLog(url, revision).splitlines() |
- if revision_log: |
- commit_title = revision_log[0] |
- # Limit title to 68 chars so git log --oneline is <80 chars. |
- max_commit_title = 68 - (len(drover_title) + 3) |
- if len(commit_title) > max_commit_title: |
- commit_title = commit_title[:max_commit_title-3] + '...' |
- drover_title += ' "%s"' % commit_title |
- out.write(drover_title + '\n\n') |
- for line in revision_log: |
- out.write('> %s\n' % line) |
- if author: |
- out.write("\nTBR=" + author) |
- out.close() |
- |
- change_cmd = 'change ' + str(revision) + " " + filename |
- if options.revertbot: |
- if sys.platform == 'win32': |
- os.environ['SVN_EDITOR'] = 'cmd.exe /c exit' |
- else: |
- os.environ['SVN_EDITOR'] = 'true' |
- runGcl(change_cmd) |
- os.unlink(filename) |
- |
- if options.local: |
- return 0 |
- |
- print author |
- print revision |
- print ("gcl upload " + str(revision) + |
- " --send_mail --no_presubmit --reviewers=" + author) |
- |
- if options.revertbot or prompt("Would you like to upload?"): |
- if PROMPT_FOR_AUTHOR or is_self_revert: |
- author = text_prompt("Enter new author or press enter to accept default", |
- author) |
- if options.revertbot and options.revertbot_reviewers: |
- author += "," |
- author += options.revertbot_reviewers |
- gclUpload(revision, author) |
- else: |
- print "Deleting the changelist." |
- print "gcl delete " + str(revision) |
- runGcl("delete " + str(revision)) |
- return 0 |
- |
- # We commit if the reverbot is set to commit automatically, or if this is |
- # not the revertbot and the user agrees. |
- if options.revertbot_commit or (not options.revertbot and |
- prompt("Would you like to commit?")): |
- print "gcl commit " + str(revision) + " --no_presubmit --force" |
- return runGcl("commit " + str(revision) + " --no_presubmit --force") |
- else: |
- return 0 |
- |
- |
-def main(): |
- option_parser = optparse.OptionParser(usage=USAGE % {"app": sys.argv[0]}) |
- option_parser.add_option('-m', '--merge', type="int", |
- help='Revision to merge from trunk to branch') |
- option_parser.add_option('-b', '--branch', |
- help='Branch to revert or merge from') |
- option_parser.add_option('-l', '--local', action='store_true', |
- help='Local working copy to merge to') |
- option_parser.add_option('-s', '--sbranch', |
- help='Source branch for merge') |
- option_parser.add_option('-r', '--revert', type="int", |
- help='Revision to revert') |
- option_parser.add_option('-w', '--workdir', |
- help='subdir to use for the revert') |
- option_parser.add_option('-u', '--url', |
- help='svn url to use for the revert') |
- option_parser.add_option('-a', '--auditor', |
- help='overrides the author for reviewer') |
- option_parser.add_option('--revertbot', action='store_true', |
- default=False) |
- option_parser.add_option('--no-alt-urls', action='store_true', |
- help='Disable heuristics used to determine svn url') |
- option_parser.add_option('--revertbot-commit', action='store_true', |
- default=False) |
- option_parser.add_option('--revertbot-reviewers') |
- options, args = option_parser.parse_args() |
- |
- if not options.merge and not options.revert: |
- option_parser.error("You need at least --merge or --revert") |
- return 1 |
- |
- if options.merge and not (options.branch or options.local): |
- option_parser.error("--merge requires --branch or --local") |
- return 1 |
- |
- if options.local and (options.revert or options.branch): |
- option_parser.error("--local cannot be used with --revert or --branch") |
- return 1 |
- |
- return drover(options, args) |
- |
- |
-if __name__ == "__main__": |
- try: |
- sys.exit(main()) |
- except KeyboardInterrupt: |
- sys.stderr.write('interrupted\n') |
- sys.exit(1) |