From d90ad9b76dfdbd30fa11ea1c6583ab3797f5f993 Mon Sep 17 00:00:00 2001 From: "laforge@chromium.org" Date: Thu, 6 Aug 2009 00:20:17 +0000 Subject: [PATCH] Drover == tool to rapidly merge/revert off of trunk and branch git-svn-id: svn://svn.chromium.org/chrome/trunk/tools/depot_tools@22565 0039d316-1c4b-4281-b951-d872f2087c98 --- drover | 27 +++++ drover.bat | 6 + drover.py | 341 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 374 insertions(+) create mode 100644 drover create mode 100644 drover.bat create mode 100644 drover.py diff --git a/drover b/drover new file mode 100644 index 0000000000..3cffd980db --- /dev/null +++ b/drover @@ -0,0 +1,27 @@ +#!/bin/sh +# Copyright (c) 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. + +# This script will try to sync the bootstrap directories and then defer control. + +base_dir=$(dirname "$0") +script=drover + +# Use the batch file as an entry point if on cygwin. +if [ "${OSTYPE}" = "cygwin" -a "${TERM}" != "xterm" ]; then + ${base_dir}/$drover.bat "$@" + exit +fi + + +# We're on POSIX (not cygwin). We can now safely look for svn checkout. +if [ "X$DEPOT_TOOLS_UPDATE" != "X0" -a -e "$base_dir/.svn" ] +then + # Update the bootstrap directory to stay up-to-date with the latest + # depot_tools. + svn -q up "$base_dir" +fi + +exec python "$base_dir/$script.py" "$@" + diff --git a/drover.bat b/drover.bat new file mode 100644 index 0000000000..e45e73ce0c --- /dev/null +++ b/drover.bat @@ -0,0 +1,6 @@ +@echo off + +setlocal +set PATH=%~dp0svn;%PATH% + +call python "%~dp0drover.py" %* \ No newline at end of file diff --git a/drover.py b/drover.py new file mode 100644 index 0000000000..b147a03c97 --- /dev/null +++ b/drover.py @@ -0,0 +1,341 @@ +import subprocess +import sys +import re +import os +import webbrowser + +def deltree(root): + """ + Removes a given directory + """ + if (not os.path.exists(root)): + return + + if sys.platform == 'win32': + os.system('rmdir /S /Q ' + root.replace('/','\\')) + else: + for name in os.listdir(root): + path = os.path.join(root, name) + if os.path.isdir(path): + deltree(path) + else: + os.unlink(path) + os.rmdir(root) + +def clobberDir(dir): + """ + Removes a given directory + """ + + if (os.path.exists(dir)): + print dir + " directory found, deleting" + #The following line was removed due to access controls in Windows + #which make os.unlink(path) calls impossible. + #deltree(dir) + os.system('rmdir /S /Q ' + dir.replace('/','\\')) + +def gclUpload(revision, author): + command = "gcl upload " + str(revision) + " --send_mail --no_try --no_presubmit --reviewers=" + author + os.system(command) +# subprocess.Popen(command, +# shell=True, +# stdout=None, +# stderr=subprocess.PIPE) +# stderr=subprocess.PIPE).stdout.readlines() +# for line in svn_info: +# match = re.search(r"Issue created. URL: (http://.+)", line) +# if match: +# return match.group(1) + + return None + +def getAuthor(url, revision): + command = 'svn info ' + url + "@"+str(revision) + svn_info = subprocess.Popen(command, + shell=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE).stdout.readlines() + for line in svn_info: + match = re.search(r"Last Changed Author: (.+)", line) + if match: + return match.group(1) + + return None + + +def getRevisionLog(url, revision): + """ + Takes an svn url and gets the associated revision. + """ + command = 'svn log ' + url + " -r"+str(revision) + svn_info = subprocess.Popen(command, + shell=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE).stdout.readlines() + rtn= "" + pos = 0 + for line in svn_info: + if (pos > 2): + rtn += line.replace('-','').replace('\r','') + else: + pos = pos + 1 + + return rtn + +def checkoutRevision(url, revision, branch_url): + paths = getBestMergePaths(url, revision) + + deltree('./src') + + if not os.path.exists('./src'): + command = 'svn checkout -N ' + branch_url + print command + os.system(command) + + #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() + + for path in paths: + subpaths = path.split('/') + subpaths.pop(0) + base = './src' + for subpath in subpaths: + base += '/' + subpath + if not os.path.exists(base): + command = ('svn update --depth empty ' + base) + print command + os.system(command) + else: + print "Found " + base + + files = getFilesInRevision(url, revision) + + for file in files: + #Prevent the tool from clobbering the src directory + if (file == ""): + continue + command = ('svn up ./src' + file) + print command + os.system(command) + +#def mergeRevision(url, revision): +# command = 'svn merge -r ' + str(revision-1) + ":" + str(revision) + " " + url +# print command +# os.system(command) + +def mergeRevision(url, revision, ignoreAncestry=False): + paths = getBestMergePaths(url, revision) + for path in paths: + command = ('svn merge -N -r ' + str(revision-1) + ":" + str(revision) + " ") + if (ignoreAncestry): + command = command + " --ignore-ancestry " + command = command + url + path + " ./src" + path + + print command + os.system(command) + +def revertRevision(url, revision): + paths = getBestMergePaths(url, revision) + for path in paths: + command = ('svn merge -N -r ' + str(revision) + ":" + str(revision-1) + " " + + url + path + " ./src" + path) + print command + os.system(command) + +def getBestMergePaths(url, revision): + """ + Takes an svn url and gets the associated revision. + """ + command = 'svn log ' + url + " -r "+str(revision) + " -v" + svn_info = subprocess.Popen(command, + shell=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE).stdout.readlines() + map = dict() + for line in svn_info: + #match = re.search(r"[\n\r ]+[MADUC][\n\r ]+/.*/src(.*)/.+", line) + #match = re.search(r"[\n\r ]+[MADUC][\n\r ]+/(?:trunk|branches/\d+)/src(.*)/.+", line) + match = re.search(r"[\n\r ]+[MADUC][\n\r ]+/(?:trunk|branches/\d+)/src([^ ]*)/[^ ]+", line) + + if match: + map[match.group(1)] = match.group(1) + + return map.keys() + +def getFilesInRevision(url, revision): + """ + Takes an svn url and gets the associated revision. + """ + command = 'svn log ' + url + " -r "+str(revision) + " -v" + svn_info = subprocess.Popen(command, + shell=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE).stdout.readlines() + map = dict() + for line in svn_info: + match = re.search(r"[\n\r ]+[MADUC][\n\r ]+/(?:trunk|branches/\d+)/src([^ ]*)/([^ ]+)", line) + + if match: + map[match.group(1) + "/" + match.group(2)] = match.group(1) + "/" + match.group(2) + + return map.keys() + +def getBestMergePath(url, revision): + """ + Takes an svn url and gets the associated revision. + """ + command = 'svn log ' + url + " -r "+str(revision) + " -v" + svn_info = subprocess.Popen(command, + shell=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE).stdout.readlines() + best_path = None + + for line in svn_info: + match = re.search(r"[\n\r ]+[MADUC][\n\r ]+/.*/src(.*)/.+", line) + if match: + if (best_path == None): + best_path = match.group(1) + else: + best_path = leastPath(match.group(1),best_path) +# print best_path + + return best_path + +def leastPath(a, b): + if (not a) or (a == ""): + return "" + if (b == ""): + return "" + if (not b): + return a + + a_list = a.lstrip("/").split("/") + b_list = b.lstrip("/").split("/") + + last_match = "" + while((len(a_list) != 0) and (len(b_list) != 0)): + a_value = a_list.pop(0) + b_value = b_list.pop(0) + if (a_value == b_value): + last_match = last_match + "/" + a_value + else: + break + + return last_match + +def prompt(question): + p = None + + while not p: + print question + " [y|n]:" + p = sys.stdin.readline() + if p.lower().startswith('n'): + return False + elif p.lower().startswith('y'): + return True + else: + p = None + +def main(argv=None): + BASE_URL = "svn://chrome-svn/chrome" + TRUNK_URL = BASE_URL + "/trunk/src" + BRANCH_URL = None + + if (len(sys.argv) == 1): + print "WARNING: Please use this tool in an empty directory (or at least one" + print "that you don't mind clobbering." + print "REQUIRES: SVN 1.5+" + print "NOTE: NO NEED TO CHECKOUT ANYTHING IN ADVANCE OF USING THIS TOOL." + print "\nValid parameters:" + print "\n[Merge from trunk to branch]" + print " --merge " + print "Example " + sys.argv[0] + " 12345 --merge 187" + print "\n[Merge from trunk to branch, ignoring revision history]" + print " --mplus " + print "Example " + sys.argv[0] + " 12345 --mplus 187" + print "\n[Revert from trunk]" + print " --revert" + print "Example " + sys.argv[0] + " 12345 --revert" + print "\n[Revert from branch]" + print " --revert " + print "Example " + sys.argv[0] + " 12345 --revert 187" + sys.exit(0) + + revision = int(sys.argv[1]) + if ((len(sys.argv) >= 4) and (sys.argv[2] in ['--revert','-r'])): + BRANCH_URL = BASE_URL + "/branches/" + sys.argv[3] + "/src" + url = BRANCH_URL + else: + url = TRUNK_URL + action = "Merge" + + command = 'svn log ' + url + " -r "+str(revision) + " -v" + os.system(command) + + if not prompt("Is this the correct revision?"): + sys.exit(0) + + if (len(sys.argv) > 1): + if sys.argv[2] in ['--merge','-m']: + if (len(sys.argv) != 4): + print "Please specify the branch # you want (i.e. 182) after --merge" + sys.exit(0) + + branch_url = "svn://chrome-svn/chrome/branches/" + sys.argv[3] + "/src" + checkoutRevision(url, revision, branch_url) + mergeRevision(url, revision) + elif sys.argv[2] in ['--mplus','-p']: + if (len(sys.argv) != 4): + print "Please specify the branch # you want (i.e. 182) after --merge" + sys.exit(0) + branch_url = "svn://chrome-svn/chrome/branches/" + sys.argv[3] + "/src" + checkoutRevision(url, revision, branch_url) + mergeRevision(url, revision, True) + elif sys.argv[2] in ['--revert','-r']: + if (len(sys.argv) == 4): + url = "svn://chrome-svn/chrome/branches/" + sys.argv[3] + "/src" + checkoutRevision(url, revision, url) + revertRevision(url, revision) + action = "Revert" + else: + print "Unknown parameter " + sys.argv[2] + sys.exit(0) + + os.chdir('./src') + + #Check the base url so we actually find the author who made the change + author = getAuthor(BASE_URL, revision) + + filename = str(revision)+".txt" + out = open(filename,"w") + out.write(action +" " + str(revision) + " - ") + out.write(getRevisionLog(url, revision)) + if (author): + out.write("TBR=" + author) + out.close() + + os.system('gcl change ' + str(revision) + " " + filename) + os.unlink(filename) + print author + print revision + print "gcl upload " + str(revision) + " --send_mail --no_try --no_presubmit --reviewers=" + author + print "gcl commit " + str(revision) + " --no_presubmit --force" + print "gcl delete " + str(revision) + + if prompt("Would you like to upload?"): + gclUpload(revision, author) + else: + print "Deleting the changelist." + os.system("gcl delete " + str(revision)) + sys.exit(0) + + if prompt("Would you like to commit?"): + os.system("gcl commit " + str(revision) + " --no_presubmit --force") + else: + sys.exit(0) + +if __name__ == "__main__": + sys.exit(main()) \ No newline at end of file