Index: depot_tools/gclient.py |
=================================================================== |
--- depot_tools/gclient.py (revision 0) |
+++ depot_tools/gclient.py (revision 0) |
@@ -0,0 +1,1649 @@ |
+#!/usr/bin/python |
+# |
+# Copyright 2008 Google Inc. All Rights Reserved. |
+# |
+# Licensed under the Apache License, Version 2.0 (the "License"); |
+# you may not use this file except in compliance with the License. |
+# You may obtain a copy of the License at |
+# |
+# http://www.apache.org/licenses/LICENSE-2.0 |
+# |
+# Unless required by applicable law or agreed to in writing, software |
+# distributed under the License is distributed on an "AS IS" BASIS, |
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
+# See the License for the specific language governing permissions and |
+# limitations under the License. |
+ |
+"""A wrapper script to manage a set of client modules in different SCM. |
+ |
+This script is intended to be used to help basic management of client |
+program sources residing in one or more Subversion modules, along with |
+other modules it depends on, also in Subversion, but possibly on |
+multiple respositories, making a wrapper system apparently necessary. |
+ |
+Files |
+ .gclient : Current client configuration, written by 'config' command. |
+ Format is a Python script defining 'solutions', a list whose |
+ entries each are maps binding the strings "name" and "url" |
+ to strings specifying the name and location of the client |
+ module, as well as "custom_deps" to a map similar to the DEPS |
+ file below. |
+ .gclient_entries : A cache constructed by 'update' command. Format is a |
+ Python script defining 'entries', a list of the names |
+ of all modules in the client |
+ <module>/DEPS : Python script defining var 'deps' as a map from each requisite |
+ submodule name to a URL where it can be found (via one SCM) |
+ |
+Hooks |
+ .gclient and DEPS files may optionally contain a list named "hooks" to |
+ allow custom actions to be performed based on files that have changed in the |
+ working copy as a result of a "sync"/"update" or "revert" operation. Hooks |
+ can also be run based on what files have been modified in the working copy |
+ with the "runhooks" operation. If any of these operation are run with |
+ --force, all known hooks will run regardless of the state of the working |
+ copy. |
+ |
+ Each item in a "hooks" list is a dict, containing these two keys: |
+ "pattern" The associated value is a string containing a regular |
+ expression. When a file whose pathname matches the expression |
+ is checked out, updated, or reverted, the hook's "action" will |
+ run. |
+ "action" A list describing a command to run along with its arguments, if |
+ any. An action command will run at most one time per gclient |
+ invocation, regardless of how many files matched the pattern. |
+ The action is executed in the same directory as the .gclient |
+ file. If the first item in the list is the string "python", |
+ the current Python interpreter (sys.executable) will be used |
+ to run the command. |
+ |
+ Example: |
+ hooks = [ |
+ { "pattern": "\\.(gif|jpe?g|pr0n|png)$", |
+ "action": ["python", "image_indexer.py", "--all"]}, |
+ ] |
+""" |
+ |
+__author__ = "darinf@gmail.com (Darin Fisher)" |
+__version__ = "0.3.1" |
+ |
+import errno |
+import optparse |
+import os |
+import re |
+import stat |
+import subprocess |
+import sys |
+import time |
+import urlparse |
+import xml.dom.minidom |
+import urllib |
+ |
+def getText(nodelist): |
+ """ |
+ Return the concatenated text for 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.append(getText(node.childNodes)) |
+ return ''.join(rc) |
+ |
+ |
+SVN_COMMAND = "svn" |
+ |
+ |
+# default help text |
+DEFAULT_USAGE_TEXT = ( |
+"""usage: %prog <subcommand> [options] [--] [svn options/args...] |
+a wrapper for managing a set of client modules in svn. |
+Version """ + __version__ + """ |
+ |
+subcommands: |
+ cleanup |
+ config |
+ diff |
+ revert |
+ status |
+ sync |
+ update |
+ runhooks |
+ revinfo |
+ |
+Options and extra arguments can be passed to invoked svn commands by |
+appending them to the command line. Note that if the first such |
+appended option starts with a dash (-) then the options must be |
+preceded by -- to distinguish them from gclient options. |
+ |
+For additional help on a subcommand or examples of usage, try |
+ %prog help <subcommand> |
+ %prog help files |
+""") |
+ |
+GENERIC_UPDATE_USAGE_TEXT = ( |
+ """Perform a checkout/update of the modules specified by the gclient |
+configuration; see 'help config'. Unless --revision is specified, |
+then the latest revision of the root solutions is checked out, with |
+dependent submodule versions updated according to DEPS files. |
+If --revision is specified, then the given revision is used in place |
+of the latest, either for a single solution or for all solutions. |
+Unless the --force option is provided, solutions and modules whose |
+local revision matches the one to update (i.e., they have not changed |
+in the repository) are *not* modified. |
+This a synonym for 'gclient %(alias)s' |
+ |
+usage: gclient %(cmd)s [options] [--] [svn update options/args] |
+ |
+Valid options: |
+ --force : force update even for unchanged modules |
+ --revision REV : update/checkout all solutions with specified revision |
+ --revision SOLUTION@REV : update given solution to specified revision |
+ --deps PLATFORM(S) : sync deps for the given platform(s), or 'all' |
+ --verbose : output additional diagnostics |
+ |
+Examples: |
+ gclient %(cmd)s |
+ update files from SVN according to current configuration, |
+ *for modules which have changed since last update or sync* |
+ gclient %(cmd)s --force |
+ update files from SVN according to current configuration, for |
+ all modules (useful for recovering files deleted from local copy) |
+""") |
+ |
+COMMAND_USAGE_TEXT = { |
+ "cleanup": |
+ """Clean up all working copies, using 'svn cleanup' for each module. |
+Additional options and args may be passed to 'svn cleanup'. |
+ |
+usage: cleanup [options] [--] [svn cleanup args/options] |
+ |
+Valid options: |
+ --verbose : output additional diagnostics |
+""", |
+ "config": """Create a .gclient file in the current directory; this |
+specifies the configuration for further commands. After update/sync, |
+top-level DEPS files in each module are read to determine dependent |
+modules to operate on as well. If optional [url] parameter is |
+provided, then configuration is read from a specified Subversion server |
+URL. Otherwise, a --spec option must be provided. |
+ |
+usage: config [option | url] [safesync url] |
+ |
+Valid options: |
+ --spec=GCLIENT_SPEC : contents of .gclient are read from string parameter. |
+ *Note that due to Cygwin/Python brokenness, it |
+ probably can't contain any newlines.* |
+ |
+Examples: |
+ gclient config https://gclient.googlecode.com/svn/trunk/gclient |
+ configure a new client to check out gclient.py tool sources |
+ gclient config --spec='solutions=[{"name":"gclient",""" |
+ '"url":"https://gclient.googlecode.com/svn/trunk/gclient",' |
+ '"custom_deps":{}}]', |
+ "diff": """Display the differences between two revisions of modules. |
+(Does 'svn diff' for each checked out module and dependences.) |
+Additional args and options to 'svn diff' can be passed after |
+gclient options. |
+ |
+usage: diff [options] [--] [svn args/options] |
+ |
+Valid options: |
+ --verbose : output additional diagnostics |
+ |
+Examples: |
+ gclient diff |
+ simple 'svn diff' for configured client and dependences |
+ gclient diff -- -x -b |
+ use 'svn diff -x -b' to suppress whitespace-only differences |
+ gclient diff -- -r HEAD -x -b |
+ diff versus the latest version of each module |
+""", |
+ "revert": |
+ """Revert every file in every managed directory in the client view. |
+ |
+usage: revert |
+""", |
+ "status": |
+ """Show the status of client and dependent modules, using 'svn diff' |
+for each module. Additional options and args may be passed to 'svn diff'. |
+ |
+usage: status [options] [--] [svn diff args/options] |
+ |
+Valid options: |
+ --verbose : output additional diagnostics |
+""", |
+ "sync": GENERIC_UPDATE_USAGE_TEXT % {"cmd": "sync", "alias": "update"}, |
+ "update": GENERIC_UPDATE_USAGE_TEXT % {"cmd": "update", "alias": "sync"}, |
+ "help": """Describe the usage of this program or its subcommands. |
+ |
+usage: help [options] [subcommand] |
+ |
+Valid options: |
+ --verbose : output additional diagnostics |
+""", |
+ "runhooks": |
+ """Runs hooks for files that have been modified in the local working copy, |
+according to 'svn status'. |
+ |
+usage: runhooks [options] |
+ |
+Valid options: |
+ --force : runs all known hooks, regardless of the working |
+ copy status |
+ --verbose : output additional diagnostics |
+""", |
+ "revinfo": |
+ """Outputs source path, server URL and revision information for every |
+dependency in all solutions (no local checkout required). |
+ |
+usage: revinfo [options] |
+""", |
+} |
+ |
+# parameterized by (solution_name, solution_url, safesync_url) |
+DEFAULT_CLIENT_FILE_TEXT = ( |
+ """ |
+# An element of this array (a \"solution\") describes a repository directory |
+# that will be checked out into your working copy. Each solution may |
+# optionally define additional dependencies (via its DEPS file) to be |
+# checked out alongside the solution's directory. A solution may also |
+# specify custom dependencies (via the \"custom_deps\" property) that |
+# override or augment the dependencies specified by the DEPS file. |
+# If a \"safesync_url\" is specified, it is assumed to reference the location of |
+# a text file which contains nothing but the last known good SCM revision to |
+# sync against. It is fetched if specified and used unless --head is passed |
+solutions = [ |
+ { \"name\" : \"%s\", |
+ \"url\" : \"%s\", |
+ \"custom_deps\" : { |
+ # To use the trunk of a component instead of what's in DEPS: |
+ #\"component\": \"https://svnserver/component/trunk/\", |
+ # To exclude a component from your working copy: |
+ #\"data/really_large_component\": None, |
+ }, |
+ \"safesync_url\": \"%s\" |
+ } |
+] |
+""") |
+ |
+ |
+## Generic utils |
+ |
+ |
+class Error(Exception): |
+ """gclient exception class.""" |
+ pass |
+ |
+class PrintableObject(object): |
+ def __str__(self): |
+ output = '' |
+ for i in dir(self): |
+ if i.startswith('__'): |
+ continue |
+ output += '%s = %s\n' % (i, str(getattr(self, i, ''))) |
+ return output |
+ |
+ |
+def FileRead(filename): |
+ content = None |
+ f = open(filename, "rU") |
+ try: |
+ content = f.read() |
+ finally: |
+ f.close() |
+ return content |
+ |
+ |
+def FileWrite(filename, content): |
+ f = open(filename, "w") |
+ try: |
+ f.write(content) |
+ finally: |
+ f.close() |
+ |
+ |
+def RemoveDirectory(*path): |
+ """Recursively removes a directory, even if it's marked read-only. |
+ |
+ Remove the directory located at *path, if it exists. |
+ |
+ shutil.rmtree() doesn't work on Windows if any of the files or directories |
+ are read-only, which svn repositories and some .svn files are. We need to |
+ be able to force the files to be writable (i.e., deletable) as we traverse |
+ the tree. |
+ |
+ Even with all this, Windows still sometimes fails to delete a file, citing |
+ a permission error (maybe something to do with antivirus scans or disk |
+ indexing). The best suggestion any of the user forums had was to wait a |
+ bit and try again, so we do that too. It's hand-waving, but sometimes it |
+ works. :/ |
+ |
+ On POSIX systems, things are a little bit simpler. The modes of the files |
+ to be deleted doesn't matter, only the modes of the directories containing |
+ them are significant. As the directory tree is traversed, each directory |
+ has its mode set appropriately before descending into it. This should |
+ result in the entire tree being removed, with the possible exception of |
+ *path itself, because nothing attempts to change the mode of its parent. |
+ Doing so would be hazardous, as it's not a directory slated for removal. |
+ In the ordinary case, this is not a problem: for our purposes, the user |
+ will never lack write permission on *path's parent. |
+ """ |
+ file_path = os.path.join(*path) |
+ if not os.path.exists(file_path): |
+ return |
+ |
+ if os.path.islink(file_path) or not os.path.isdir(file_path): |
+ raise Error("RemoveDirectory asked to remove non-directory %s" % file_path) |
+ |
+ has_win32api = False |
+ if sys.platform == 'win32': |
+ has_win32api = True |
+ # Some people don't have the APIs installed. In that case we'll do without. |
+ try: |
+ win32api = __import__('win32api') |
+ win32con = __import__('win32con') |
+ except ImportError: |
+ has_win32api = False |
+ else: |
+ # On POSIX systems, we need the x-bit set on the directory to access it, |
+ # the r-bit to see its contents, and the w-bit to remove files from it. |
+ # The actual modes of the files within the directory is irrelevant. |
+ os.chmod(file_path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR) |
+ for fn in os.listdir(file_path): |
+ fullpath = os.path.join(file_path, fn) |
+ |
+ # If fullpath is a symbolic link that points to a directory, isdir will |
+ # be True, but we don't want to descend into that as a directory, we just |
+ # want to remove the link. Check islink and treat links as ordinary files |
+ # would be treated regardless of what they reference. |
+ if os.path.islink(fullpath) or not os.path.isdir(fullpath): |
+ if sys.platform == 'win32': |
+ os.chmod(fullpath, stat.S_IWRITE) |
+ if has_win32api: |
+ win32api.SetFileAttributes(fullpath, win32con.FILE_ATTRIBUTE_NORMAL) |
+ try: |
+ os.remove(fullpath) |
+ except OSError, e: |
+ if e.errno != errno.EACCES or sys.platform != 'win32': |
+ raise |
+ print 'Failed to delete %s: trying again' % fullpath |
+ time.sleep(0.1) |
+ os.remove(fullpath) |
+ else: |
+ RemoveDirectory(fullpath) |
+ |
+ if sys.platform == 'win32': |
+ os.chmod(file_path, stat.S_IWRITE) |
+ if has_win32api: |
+ win32api.SetFileAttributes(file_path, win32con.FILE_ATTRIBUTE_NORMAL) |
+ try: |
+ os.rmdir(file_path) |
+ except OSError, e: |
+ if e.errno != errno.EACCES or sys.platform != 'win32': |
+ raise |
+ print 'Failed to remove %s: trying again' % file_path |
+ time.sleep(0.1) |
+ os.rmdir(file_path) |
+ |
+ |
+def SubprocessCall(command, in_directory, out, fail_status=None): |
+ """Runs command, a list, in directory in_directory. |
+ |
+ This function wraps SubprocessCallAndCapture, but does not perform the |
+ capturing functions. See that function for a more complete usage |
+ description. |
+ """ |
+ # Call subprocess and capture nothing: |
+ SubprocessCallAndCapture(command, in_directory, out, fail_status) |
+ |
+ |
+def SubprocessCallAndCapture(command, in_directory, out, fail_status=None, |
+ pattern=None, capture_list=None): |
+ """Runs command, a list, in directory in_directory. |
+ |
+ A message indicating what is being done, as well as the command's stdout, |
+ is printed to out. |
+ |
+ If a pattern is specified, any line in the output matching pattern will have |
+ its first match group appended to capture_list. |
+ |
+ If the command fails, as indicated by a nonzero exit status, gclient will |
+ exit with an exit status of fail_status. If fail_status is None (the |
+ default), gclient will raise an Error exception. |
+ """ |
+ |
+ print >> out, ("\n________ running \'%s\' in \'%s\'" |
+ % (' '.join(command), in_directory)) |
+ |
+ # *Sigh*: Windows needs shell=True, or else it won't search %PATH% for the |
+ # executable, but shell=True makes subprocess on Linux fail when it's called |
+ # with a list because it only tries to execute the first item in the list. |
+ kid = subprocess.Popen(command, bufsize=0, cwd=in_directory, |
+ shell=(sys.platform == 'win32'), stdout=subprocess.PIPE) |
+ |
+ if pattern: |
+ compiled_pattern = re.compile(pattern) |
+ |
+ # Also, we need to forward stdout to prevent weird re-ordering of output. |
+ # This has to be done on a per byte basis to make sure it is not buffered: |
+ # normally buffering is done for each line, but if svn requests input, no |
+ # end-of-line character is output after the prompt and it would not show up. |
+ in_byte = kid.stdout.read(1) |
+ in_line = "" |
+ while in_byte: |
+ if in_byte != "\r": |
+ out.write(in_byte) |
+ in_line += in_byte |
+ if in_byte == "\n" and pattern: |
+ match = compiled_pattern.search(in_line[:-1]) |
+ if match: |
+ capture_list.append(match.group(1)) |
+ in_line = "" |
+ in_byte = kid.stdout.read(1) |
+ rv = kid.wait() |
+ |
+ if rv: |
+ msg = "failed to run command: %s" % " ".join(command) |
+ |
+ if fail_status != None: |
+ print >>sys.stderr, msg |
+ sys.exit(fail_status) |
+ |
+ raise Error(msg) |
+ |
+ |
+def IsUsingGit(root, paths): |
+ """Returns True if we're using git to manage any of our checkouts. |
+ |entries| is a list of paths to check.""" |
+ for path in paths: |
+ if os.path.exists(os.path.join(root, path, '.git')): |
+ return True |
+ return False |
+ |
+# ----------------------------------------------------------------------------- |
+# SVN utils: |
+ |
+ |
+def RunSVN(options, args, in_directory): |
+ """Runs svn, sending output to stdout. |
+ |
+ Args: |
+ args: A sequence of command line parameters to be passed to svn. |
+ in_directory: The directory where svn is to be run. |
+ |
+ Raises: |
+ Error: An error occurred while running the svn command. |
+ """ |
+ c = [SVN_COMMAND] |
+ c.extend(args) |
+ |
+ SubprocessCall(c, in_directory, options.stdout) |
+ |
+ |
+def CaptureSVN(options, args, in_directory): |
+ """Runs svn, capturing output sent to stdout as a string. |
+ |
+ Args: |
+ args: A sequence of command line parameters to be passed to svn. |
+ in_directory: The directory where svn is to be run. |
+ |
+ Returns: |
+ The output sent to stdout as a string. |
+ """ |
+ c = [SVN_COMMAND] |
+ c.extend(args) |
+ |
+ # *Sigh*: Windows needs shell=True, or else it won't search %PATH% for |
+ # the svn.exe executable, but shell=True makes subprocess on Linux fail |
+ # when it's called with a list because it only tries to execute the |
+ # first string ("svn"). |
+ return subprocess.Popen(c, cwd=in_directory, shell=(sys.platform == 'win32'), |
+ stdout=subprocess.PIPE).communicate()[0] |
+ |
+ |
+def RunSVNAndGetFileList(options, args, in_directory, file_list): |
+ """Runs svn checkout, update, or status, output to stdout. |
+ |
+ The first item in args must be either "checkout", "update", or "status". |
+ |
+ svn's stdout is parsed to collect a list of files checked out or updated. |
+ These files are appended to file_list. svn's stdout is also printed to |
+ sys.stdout as in RunSVN. |
+ |
+ Args: |
+ args: A sequence of command line parameters to be passed to svn. |
+ in_directory: The directory where svn is to be run. |
+ |
+ Raises: |
+ Error: An error occurred while running the svn command. |
+ """ |
+ command = [SVN_COMMAND] |
+ command.extend(args) |
+ |
+ # svn update and svn checkout use the same pattern: the first three columns |
+ # are for file status, property status, and lock status. This is followed |
+ # by two spaces, and then the path to the file. |
+ update_pattern = '^... (.*)$' |
+ |
+ # The first three columns of svn status are the same as for svn update and |
+ # svn checkout. The next three columns indicate addition-with-history, |
+ # switch, and remote lock status. This is followed by one space, and then |
+ # the path to the file. |
+ status_pattern = '^...... (.*)$' |
+ |
+ # args[0] must be a supported command. This will blow up if it's something |
+ # else, which is good. Note that the patterns are only effective when |
+ # these commands are used in their ordinary forms, the patterns are invalid |
+ # for "svn status --show-updates", for example. |
+ pattern = { |
+ 'checkout': update_pattern, |
+ 'status': status_pattern, |
+ 'update': update_pattern, |
+ }[args[0]] |
+ |
+ SubprocessCallAndCapture(command, in_directory, options.stdout, |
+ pattern=pattern, capture_list=file_list) |
+ |
+ |
+def CaptureSVNInfo(options, relpath, in_directory): |
+ """Runs 'svn info' on an existing path. |
+ |
+ Args: |
+ relpath: The directory where the working copy resides relative to |
+ the directory given by in_directory. |
+ in_directory: The directory where svn is to be run. |
+ |
+ Returns: |
+ An object with fields corresponding to the output of 'svn info' |
+ """ |
+ info = CaptureSVN(options, ["info", "--xml", relpath], in_directory) |
+ dom = xml.dom.minidom.parseString(info) |
+ |
+ # str() the getText() results because they may be returned as |
+ # Unicode, which interferes with the higher layers matching up |
+ # things in the deps dictionary. |
+ result = PrintableObject() |
+ result.root = str(getText(dom.getElementsByTagName('root'))) |
+ result.url = str(getText(dom.getElementsByTagName('url'))) |
+ result.uuid = str(getText(dom.getElementsByTagName('uuid'))) |
+ result.revision = int(dom.getElementsByTagName('entry')[0].getAttribute( |
+ 'revision')) |
+ return result |
+ |
+ |
+def CaptureSVNHeadRevision(options, url): |
+ """Get the head revision of a SVN repository. |
+ |
+ Returns: |
+ Int head revision |
+ """ |
+ info = CaptureSVN(options, ["info", "--xml", url], os.getcwd()) |
+ dom = xml.dom.minidom.parseString(info) |
+ return int(dom.getElementsByTagName('entry')[0].getAttribute('revision')) |
+ |
+ |
+class FileStatus: |
+ def __init__(self, path, text_status, props, locked, history, switched, |
+ repo_locked, out_of_date): |
+ self.path = path.strip() |
+ self.text_status = text_status |
+ self.props = props |
+ self.locked = locked |
+ self.history = history |
+ self.switched = switched |
+ self.repo_locked = repo_locked |
+ self.out_of_date = out_of_date |
+ |
+ def __str__(self): |
+ return (self.text_status + self.props + self.locked + self.history + |
+ self.switched + self.repo_locked + self.out_of_date + |
+ self.path) |
+ |
+ |
+def CaptureSVNStatus(options, path): |
+ """Runs 'svn status' on an existing path. |
+ |
+ Args: |
+ path: The directory to run svn status. |
+ |
+ Returns: |
+ An array of FileStatus corresponding to the output of 'svn status' |
+ """ |
+ info = CaptureSVN(options, ["status"], path) |
+ result = [] |
+ if not info: |
+ return result |
+ for line in info.splitlines(): |
+ if line: |
+ new_item = FileStatus(line[7:], line[0:1], line[1:2], line[2:3], |
+ line[3:4], line[4:5], line[5:6], line[6:7]) |
+ result.append(new_item) |
+ return result |
+ |
+ |
+### SCM abstraction layer |
+ |
+ |
+class SCMWrapper(object): |
+ """Add necessary glue between all the supported SCM. |
+ |
+ This is the abstraction layer to bind to different SCM. Since currently only |
+ subversion is supported, a lot of subersionism remains. This can be sorted out |
+ once another SCM is supported.""" |
+ def __init__(self, url=None, root_dir=None, relpath=None, |
+ scm_name='svn'): |
+ # TODO(maruel): Deduce the SCM from the url. |
+ self.scm_name = scm_name |
+ self.url = url |
+ self._root_dir = root_dir |
+ if self._root_dir: |
+ self._root_dir = self._root_dir.replace('/', os.sep).strip() |
+ self.relpath = relpath |
+ if self.relpath: |
+ self.relpath = self.relpath.replace('/', os.sep).strip() |
+ |
+ def FullUrlForRelativeUrl(self, url): |
+ # Find the forth '/' and strip from there. A bit hackish. |
+ return '/'.join(self.url.split('/')[:4]) + url |
+ |
+ def RunCommand(self, command, options, args, file_list=None): |
+ # file_list will have all files that are modified appended to it. |
+ |
+ if file_list == None: |
+ file_list = [] |
+ |
+ commands = { |
+ 'cleanup': self.cleanup, |
+ 'update': self.update, |
+ 'revert': self.revert, |
+ 'status': self.status, |
+ 'diff': self.diff, |
+ 'runhooks': self.status, |
+ } |
+ |
+ if not command in commands: |
+ raise Error('Unknown command %s' % command) |
+ |
+ return commands[command](options, args, file_list) |
+ |
+ def cleanup(self, options, args, file_list): |
+ """Cleanup working copy.""" |
+ command = ['cleanup'] |
+ command.extend(args) |
+ RunSVN(options, command, os.path.join(self._root_dir, self.relpath)) |
+ |
+ def diff(self, options, args, file_list): |
+ # NOTE: This function does not currently modify file_list. |
+ command = ['diff'] |
+ command.extend(args) |
+ RunSVN(options, command, os.path.join(self._root_dir, self.relpath)) |
+ |
+ def update(self, options, args, file_list): |
+ """Runs SCM to update or transparently checkout the working copy. |
+ |
+ All updated files will be appended to file_list. |
+ |
+ Raises: |
+ Error: if can't get URL for relative path. |
+ """ |
+ # Only update if git is not controlling the directory. |
+ git_path = os.path.join(self._root_dir, self.relpath, '.git') |
+ if options.path_exists(git_path): |
+ print >> options.stdout, ( |
+ "________ found .git directory; skipping %s" % self.relpath) |
+ return |
+ |
+ if args: |
+ raise Error("Unsupported argument(s): %s" % ",".join(args)) |
+ |
+ url = self.url |
+ components = url.split("@") |
+ revision = None |
+ forced_revision = False |
+ if options.revision: |
+ # Override the revision number. |
+ url = '%s@%s' % (components[0], str(options.revision)) |
+ revision = int(options.revision) |
+ forced_revision = True |
+ elif len(components) == 2: |
+ revision = int(components[1]) |
+ forced_revision = True |
+ |
+ rev_str = "" |
+ if revision: |
+ rev_str = ' at %d' % revision |
+ |
+ if not options.path_exists(os.path.join(self._root_dir, self.relpath)): |
+ # We need to checkout. |
+ command = ['checkout', url, os.path.join(self._root_dir, self.relpath)] |
+ RunSVNAndGetFileList(options, command, self._root_dir, file_list) |
+ |
+ # Get the existing scm url and the revision number of the current checkout. |
+ from_info = CaptureSVNInfo(options, |
+ os.path.join(self._root_dir, self.relpath, '.'), |
+ '.') |
+ |
+ if options.manually_grab_svn_rev: |
+ # Retrieve the current HEAD version because svn is slow at null updates. |
+ if not revision: |
+ from_info_live = CaptureSVNInfo(options, from_info.url, '.') |
+ revision = int(from_info_live.revision) |
+ rev_str = ' at %d' % revision |
+ |
+ if from_info.url != components[0]: |
+ to_info = CaptureSVNInfo(options, url, '.') |
+ if from_info.root != to_info.root: |
+ # We have different roots, so check if we can switch --relocate. |
+ # Subversion only permits this if the repository UUIDs match. |
+ if from_info.uuid != to_info.uuid: |
+ raise Error("Can't switch the checkout to %s; UUID don't match" % url) |
+ |
+ # Perform the switch --relocate, then rewrite the from_url |
+ # to reflect where we "are now." (This is the same way that |
+ # Subversion itself handles the metadata when switch --relocate |
+ # is used.) This makes the checks below for whether we |
+ # can update to a revision or have to switch to a different |
+ # branch work as expected. |
+ # TODO(maruel): TEST ME ! |
+ command = ["switch", "--relocate", from_info.root, to_info.root, |
+ self.relpath] |
+ RunSVN(options, command, self._root_dir) |
+ from_info.url = from_info.url.replace(from_info.root, to_info.root) |
+ |
+ # If the provided url has a revision number that matches the revision |
+ # number of the existing directory, then we don't need to bother updating. |
+ if not options.force and from_info.revision == revision: |
+ if options.verbose or not forced_revision: |
+ print >>options.stdout, ("\n_____ %s%s" % ( |
+ self.relpath, rev_str)) |
+ return |
+ |
+ command = ["update", os.path.join(self._root_dir, self.relpath)] |
+ if revision: |
+ command.extend(['--revision', str(revision)]) |
+ RunSVNAndGetFileList(options, command, self._root_dir, file_list) |
+ |
+ def revert(self, options, args, file_list): |
+ """Reverts local modifications. Subversion specific. |
+ |
+ All reverted files will be appended to file_list, even if Subversion |
+ doesn't know about them. |
+ """ |
+ path = os.path.join(self._root_dir, self.relpath) |
+ if not os.path.isdir(path): |
+ # We can't revert path that doesn't exist. |
+ # TODO(maruel): Should we update instead? |
+ if options.verbose: |
+ print >>options.stdout, ("\n_____ %s is missing, can't revert" % |
+ self.relpath) |
+ return |
+ |
+ files = CaptureSVNStatus(options, path) |
+ # Batch the command. |
+ files_to_revert = [] |
+ for file in files: |
+ file_path = os.path.join(path, file.path) |
+ print >>options.stdout, file_path |
+ # Unversioned file or unexpected unversioned file. |
+ if file.text_status in ('?', '~'): |
+ # Remove extraneous file. Also remove unexpected unversioned |
+ # directories. svn won't touch them but we want to delete these. |
+ file_list.append(file_path) |
+ try: |
+ os.remove(file_path) |
+ except EnvironmentError: |
+ RemoveDirectory(file_path) |
+ |
+ if file.text_status != '?': |
+ # For any other status, svn revert will work. |
+ file_list.append(file_path) |
+ files_to_revert.append(file.path) |
+ |
+ # Revert them all at once. |
+ if files_to_revert: |
+ accumulated_paths = [] |
+ accumulated_length = 0 |
+ command = ['revert'] |
+ for p in files_to_revert: |
+ # Some shell have issues with command lines too long. |
+ if accumulated_length and accumulated_length + len(p) > 3072: |
+ RunSVN(options, command + accumulated_paths, |
+ os.path.join(self._root_dir, self.relpath)) |
+ accumulated_paths = [] |
+ accumulated_length = 0 |
+ else: |
+ accumulated_paths.append(p) |
+ accumulated_length += len(p) |
+ if accumulated_paths: |
+ RunSVN(options, command + accumulated_paths, |
+ os.path.join(self._root_dir, self.relpath)) |
+ |
+ def status(self, options, args, file_list): |
+ """Display status information.""" |
+ command = ['status'] |
+ command.extend(args) |
+ RunSVNAndGetFileList(options, command, |
+ os.path.join(self._root_dir, self.relpath), file_list) |
+ |
+ |
+## GClient implementation. |
+ |
+ |
+class GClient(object): |
+ """Object that represent a gclient checkout.""" |
+ |
+ supported_commands = [ |
+ 'cleanup', 'diff', 'revert', 'status', 'update', 'runhooks' |
+ ] |
+ |
+ def __init__(self, root_dir, options): |
+ self._root_dir = root_dir |
+ self._options = options |
+ self._config_content = None |
+ self._config_dict = {} |
+ self._deps_hooks = [] |
+ |
+ def SetConfig(self, content): |
+ self._config_dict = {} |
+ self._config_content = content |
+ exec(content, self._config_dict) |
+ |
+ def SaveConfig(self): |
+ FileWrite(os.path.join(self._root_dir, self._options.config_filename), |
+ self._config_content) |
+ |
+ def _LoadConfig(self): |
+ client_source = FileRead(os.path.join(self._root_dir, |
+ self._options.config_filename)) |
+ self.SetConfig(client_source) |
+ |
+ def ConfigContent(self): |
+ return self._config_content |
+ |
+ def GetVar(self, key, default=None): |
+ return self._config_dict.get(key, default) |
+ |
+ @staticmethod |
+ def LoadCurrentConfig(options, from_dir=None): |
+ """Searches for and loads a .gclient file relative to the current working |
+ dir. |
+ |
+ Returns: |
+ A dict representing the contents of the .gclient file or an empty dict if |
+ the .gclient file doesn't exist. |
+ """ |
+ if not from_dir: |
+ from_dir = os.curdir |
+ path = os.path.realpath(from_dir) |
+ while not options.path_exists(os.path.join(path, options.config_filename)): |
+ next = os.path.split(path) |
+ if not next[1]: |
+ return None |
+ path = next[0] |
+ client = options.gclient(path, options) |
+ client._LoadConfig() |
+ return client |
+ |
+ def SetDefaultConfig(self, solution_name, solution_url, safesync_url): |
+ self.SetConfig(DEFAULT_CLIENT_FILE_TEXT % ( |
+ solution_name, solution_url, safesync_url |
+ )) |
+ |
+ def _SaveEntries(self, entries): |
+ """Creates a .gclient_entries file to record the list of unique checkouts. |
+ |
+ The .gclient_entries file lives in the same directory as .gclient. |
+ |
+ Args: |
+ entries: A sequence of solution names. |
+ """ |
+ text = "entries = [\n" |
+ for entry in entries: |
+ text += " \"%s\",\n" % entry |
+ text += "]\n" |
+ FileWrite(os.path.join(self._root_dir, self._options.entries_filename), |
+ text) |
+ |
+ def _ReadEntries(self): |
+ """Read the .gclient_entries file for the given client. |
+ |
+ Args: |
+ client: The client for which the entries file should be read. |
+ |
+ Returns: |
+ A sequence of solution names, which will be empty if there is the |
+ entries file hasn't been created yet. |
+ """ |
+ scope = {} |
+ filename = os.path.join(self._root_dir, self._options.entries_filename) |
+ if not self._options.path_exists(filename): |
+ return [] |
+ exec(FileRead(filename), scope) |
+ return scope["entries"] |
+ |
+ class FromImpl: |
+ """Used to implement the From syntax.""" |
+ |
+ def __init__(self, module_name): |
+ self.module_name = module_name |
+ |
+ def __str__(self): |
+ return 'From("%s")' % self.module_name |
+ |
+ class _VarImpl: |
+ def __init__(self, custom_vars, local_scope): |
+ self._custom_vars = custom_vars |
+ self._local_scope = local_scope |
+ |
+ def Lookup(self, var_name): |
+ """Implements the Var syntax.""" |
+ if var_name in self._custom_vars: |
+ return self._custom_vars[var_name] |
+ elif var_name in self._local_scope.get("vars", {}): |
+ return self._local_scope["vars"][var_name] |
+ raise Error("Var is not defined: %s" % var_name) |
+ |
+ def _ParseSolutionDeps(self, solution_name, solution_deps_content, |
+ custom_vars): |
+ """Parses the DEPS file for the specified solution. |
+ |
+ Args: |
+ solution_name: The name of the solution to query. |
+ solution_deps_content: Content of the DEPS file for the solution |
+ custom_vars: A dict of vars to override any vars defined in the DEPS file. |
+ |
+ Returns: |
+ A dict mapping module names (as relative paths) to URLs or an empty |
+ dict if the solution does not have a DEPS file. |
+ """ |
+ # Skip empty |
+ if not solution_deps_content: |
+ return {} |
+ # Eval the content |
+ local_scope = {} |
+ var = self._VarImpl(custom_vars, local_scope) |
+ global_scope = {"From": self.FromImpl, "Var": var.Lookup, "deps_os": {}} |
+ exec(solution_deps_content, global_scope, local_scope) |
+ deps = local_scope.get("deps", {}) |
+ |
+ # load os specific dependencies if defined. these dependencies may |
+ # override or extend the values defined by the 'deps' member. |
+ if "deps_os" in local_scope: |
+ deps_os_choices = { |
+ "win32": "win", |
+ "win": "win", |
+ "cygwin": "win", |
+ "darwin": "mac", |
+ "mac": "mac", |
+ "unix": "unix", |
+ "linux": "unix", |
+ "linux2": "unix", |
+ } |
+ |
+ if self._options.deps_os is not None: |
+ deps_to_include = self._options.deps_os.split(",") |
+ if "all" in deps_to_include: |
+ deps_to_include = deps_os_choices.values() |
+ else: |
+ deps_to_include = [deps_os_choices.get(self._options.platform, "unix")] |
+ |
+ deps_to_include = set(deps_to_include) |
+ for deps_os_key in deps_to_include: |
+ os_deps = local_scope["deps_os"].get(deps_os_key, {}) |
+ if len(deps_to_include) > 1: |
+ # Ignore any overrides when including deps for more than one |
+ # platform, so we collect the broadest set of dependencies available. |
+ # We may end up with the wrong revision of something for our |
+ # platform, but this is the best we can do. |
+ deps.update([x for x in os_deps.items() if not x[0] in deps]) |
+ else: |
+ deps.update(os_deps) |
+ |
+ if 'hooks' in local_scope: |
+ self._deps_hooks.extend(local_scope['hooks']) |
+ |
+ # If use_relative_paths is set in the DEPS file, regenerate |
+ # the dictionary using paths relative to the directory containing |
+ # the DEPS file. |
+ if local_scope.get('use_relative_paths'): |
+ rel_deps = {} |
+ for d, url in deps.items(): |
+ # normpath is required to allow DEPS to use .. in their |
+ # dependency local path. |
+ rel_deps[os.path.normpath(os.path.join(solution_name, d))] = url |
+ return rel_deps |
+ else: |
+ return deps |
+ |
+ def _ParseAllDeps(self, solution_urls, solution_deps_content): |
+ """Parse the complete list of dependencies for the client. |
+ |
+ Args: |
+ solution_urls: A dict mapping module names (as relative paths) to URLs |
+ corresponding to the solutions specified by the client. This parameter |
+ is passed as an optimization. |
+ solution_deps_content: A dict mapping module names to the content |
+ of their DEPS files |
+ |
+ Returns: |
+ A dict mapping module names (as relative paths) to URLs corresponding |
+ to the entire set of dependencies to checkout for the given client. |
+ |
+ Raises: |
+ Error: If a dependency conflicts with another dependency or of a solution. |
+ """ |
+ deps = {} |
+ for solution in self.GetVar("solutions"): |
+ custom_vars = solution.get("custom_vars", {}) |
+ solution_deps = self._ParseSolutionDeps( |
+ solution["name"], |
+ solution_deps_content[solution["name"]], |
+ custom_vars) |
+ |
+ # If a line is in custom_deps, but not in the solution, we want to append |
+ # this line to the solution. |
+ if "custom_deps" in solution: |
+ for d in solution["custom_deps"]: |
+ if d not in solution_deps: |
+ solution_deps[d] = solution["custom_deps"][d] |
+ |
+ for d in solution_deps: |
+ if "custom_deps" in solution and d in solution["custom_deps"]: |
+ # Dependency is overriden. |
+ url = solution["custom_deps"][d] |
+ if url is None: |
+ continue |
+ else: |
+ url = solution_deps[d] |
+ # if we have a From reference dependent on another solution, then |
+ # just skip the From reference. When we pull deps for the solution, |
+ # we will take care of this dependency. |
+ # |
+ # If multiple solutions all have the same From reference, then we |
+ # should only add one to our list of dependencies. |
+ if type(url) != str: |
+ if url.module_name in solution_urls: |
+ # Already parsed. |
+ continue |
+ if d in deps and type(deps[d]) != str: |
+ if url.module_name == deps[d].module_name: |
+ continue |
+ else: |
+ parsed_url = urlparse.urlparse(url) |
+ scheme = parsed_url[0] |
+ if not scheme: |
+ # A relative url. Fetch the real base. |
+ path = parsed_url[2] |
+ if path[0] != "/": |
+ raise Error( |
+ "relative DEPS entry \"%s\" must begin with a slash" % d) |
+ # Create a scm just to query the full url. |
+ scm = self._options.scm_wrapper(solution["url"], self._root_dir, |
+ None) |
+ url = scm.FullUrlForRelativeUrl(url) |
+ if d in deps and deps[d] != url: |
+ raise Error( |
+ "Solutions have conflicting versions of dependency \"%s\"" % d) |
+ if d in solution_urls and solution_urls[d] != url: |
+ raise Error( |
+ "Dependency \"%s\" conflicts with specified solution" % d) |
+ # Grab the dependency. |
+ deps[d] = url |
+ return deps |
+ |
+ def _RunHookAction(self, hook_dict): |
+ """Runs the action from a single hook. |
+ """ |
+ command = hook_dict['action'][:] |
+ if command[0] == 'python': |
+ # If the hook specified "python" as the first item, the action is a |
+ # Python script. Run it by starting a new copy of the same |
+ # interpreter. |
+ command[0] = sys.executable |
+ |
+ # Use a discrete exit status code of 2 to indicate that a hook action |
+ # failed. Users of this script may wish to treat hook action failures |
+ # differently from VC failures. |
+ SubprocessCall(command, self._root_dir, self._options.stdout, |
+ fail_status=2) |
+ |
+ def _RunHooks(self, command, file_list, is_using_git): |
+ """Evaluates all hooks, running actions as needed. |
+ """ |
+ # Hooks only run for these command types. |
+ if not command in ('update', 'revert', 'runhooks'): |
+ return |
+ |
+ # Get any hooks from the .gclient file. |
+ hooks = self.GetVar("hooks", []) |
+ # Add any hooks found in DEPS files. |
+ hooks.extend(self._deps_hooks) |
+ |
+ # If "--force" was specified, run all hooks regardless of what files have |
+ # changed. If the user is using git, then we don't know what files have |
+ # changed so we always run all hooks. |
+ if self._options.force or is_using_git: |
+ for hook_dict in hooks: |
+ self._RunHookAction(hook_dict) |
+ return |
+ |
+ # Run hooks on the basis of whether the files from the gclient operation |
+ # match each hook's pattern. |
+ for hook_dict in hooks: |
+ pattern = re.compile(hook_dict['pattern']) |
+ for file in file_list: |
+ if not pattern.search(file): |
+ continue |
+ |
+ self._RunHookAction(hook_dict) |
+ |
+ # The hook's action only runs once. Don't bother looking for any |
+ # more matches. |
+ break |
+ |
+ def RunOnDeps(self, command, args): |
+ """Runs a command on each dependency in a client and its dependencies. |
+ |
+ The module's dependencies are specified in its top-level DEPS files. |
+ |
+ Args: |
+ command: The command to use (e.g., 'status' or 'diff') |
+ args: list of str - extra arguments to add to the command line. |
+ |
+ Raises: |
+ Error: If the client has conflicting entries. |
+ """ |
+ if not command in self.supported_commands: |
+ raise Error("'%s' is an unsupported command" % command) |
+ |
+ # Check for revision overrides. |
+ revision_overrides = {} |
+ for revision in self._options.revisions: |
+ if revision.find("@") == -1: |
+ raise Error( |
+ "Specify the full dependency when specifying a revision number.") |
+ revision_elem = revision.split("@") |
+ # Disallow conflicting revs |
+ if revision_overrides.has_key(revision_elem[0]) and \ |
+ revision_overrides[revision_elem[0]] != revision_elem[1]: |
+ raise Error( |
+ "Conflicting revision numbers specified.") |
+ revision_overrides[revision_elem[0]] = revision_elem[1] |
+ |
+ solutions = self.GetVar("solutions") |
+ if not solutions: |
+ raise Error("No solution specified") |
+ |
+ # When running runhooks --force, there's no need to consult the SCM. |
+ # All known hooks are expected to run unconditionally regardless of working |
+ # copy state, so skip the SCM status check. |
+ run_scm = not (command == 'runhooks' and self._options.force) |
+ |
+ entries = {} |
+ entries_deps_content = {} |
+ file_list = [] |
+ # Run on the base solutions first. |
+ for solution in solutions: |
+ name = solution["name"] |
+ if name in entries: |
+ raise Error("solution %s specified more than once" % name) |
+ url = solution["url"] |
+ entries[name] = url |
+ if run_scm: |
+ self._options.revision = revision_overrides.get(name) |
+ scm = self._options.scm_wrapper(url, self._root_dir, name) |
+ scm.RunCommand(command, self._options, args, file_list) |
+ self._options.revision = None |
+ try: |
+ deps_content = FileRead(os.path.join(self._root_dir, name, |
+ self._options.deps_file)) |
+ except IOError, e: |
+ if e.errno != errno.ENOENT: |
+ raise |
+ deps_content = "" |
+ entries_deps_content[name] = deps_content |
+ |
+ # Process the dependencies next (sort alphanumerically to ensure that |
+ # containing directories get populated first and for readability) |
+ deps = self._ParseAllDeps(entries, entries_deps_content) |
+ deps_to_process = deps.keys() |
+ deps_to_process.sort() |
+ |
+ # First pass for direct dependencies. |
+ for d in deps_to_process: |
+ if type(deps[d]) == str: |
+ url = deps[d] |
+ entries[d] = url |
+ if run_scm: |
+ self._options.revision = revision_overrides.get(d) |
+ scm = self._options.scm_wrapper(url, self._root_dir, d) |
+ scm.RunCommand(command, self._options, args, file_list) |
+ self._options.revision = None |
+ |
+ # Second pass for inherited deps (via the From keyword) |
+ for d in deps_to_process: |
+ if type(deps[d]) != str: |
+ sub_deps = self._ParseSolutionDeps( |
+ deps[d].module_name, |
+ FileRead(os.path.join(self._root_dir, |
+ deps[d].module_name, |
+ self._options.deps_file)), |
+ {}) |
+ url = sub_deps[d] |
+ entries[d] = url |
+ if run_scm: |
+ self._options.revision = revision_overrides.get(d) |
+ scm = self._options.scm_wrapper(url, self._root_dir, d) |
+ scm.RunCommand(command, self._options, args, file_list) |
+ self._options.revision = None |
+ |
+ is_using_git = IsUsingGit(self._root_dir, entries.keys()) |
+ self._RunHooks(command, file_list, is_using_git) |
+ |
+ if command == 'update': |
+ # notify the user if there is an orphaned entry in their working copy. |
+ # TODO(darin): we should delete this directory manually if it doesn't |
+ # have any changes in it. |
+ prev_entries = self._ReadEntries() |
+ for entry in prev_entries: |
+ e_dir = os.path.join(self._root_dir, entry) |
+ if entry not in entries and self._options.path_exists(e_dir): |
+ if CaptureSVNStatus(self._options, e_dir): |
+ # There are modified files in this entry |
+ entries[entry] = None # Keep warning until removed. |
+ print >> self._options.stdout, ( |
+ "\nWARNING: \"%s\" is no longer part of this client. " |
+ "It is recommended that you manually remove it.\n") % entry |
+ else: |
+ # Delete the entry |
+ print >> self._options.stdout, ("\n________ deleting \'%s\' " + |
+ "in \'%s\'") % (entry, self._root_dir) |
+ RemoveDirectory(e_dir) |
+ # record the current list of entries for next time |
+ self._SaveEntries(entries) |
+ |
+ def PrintRevInfo(self): |
+ """Output revision info mapping for the client and its dependencies. This |
+ allows the capture of a overall "revision" for the source tree that can |
+ be used to reproduce the same tree in the future. The actual output |
+ contains enough information (source paths, svn server urls and revisions) |
+ that it can be used either to generate external svn commands (without |
+ gclient) or as input to gclient's --rev option (with some massaging of |
+ the data). |
+ |
+ NOTE: Unlike RunOnDeps this does not require a local checkout and is run |
+ on the Pulse master. It MUST NOT execute hooks. |
+ |
+ Raises: |
+ Error: If the client has conflicting entries. |
+ """ |
+ # Check for revision overrides. |
+ revision_overrides = {} |
+ for revision in self._options.revisions: |
+ if revision.find("@") < 0: |
+ raise Error( |
+ "Specify the full dependency when specifying a revision number.") |
+ revision_elem = revision.split("@") |
+ # Disallow conflicting revs |
+ if revision_overrides.has_key(revision_elem[0]) and \ |
+ revision_overrides[revision_elem[0]] != revision_elem[1]: |
+ raise Error( |
+ "Conflicting revision numbers specified.") |
+ revision_overrides[revision_elem[0]] = revision_elem[1] |
+ |
+ solutions = self.GetVar("solutions") |
+ if not solutions: |
+ raise Error("No solution specified") |
+ |
+ entries = {} |
+ entries_deps_content = {} |
+ |
+ # Inner helper to generate base url and rev tuple (including honoring |
+ # |revision_overrides|) |
+ def GetURLAndRev(name, original_url): |
+ if original_url.find("@") < 0: |
+ if revision_overrides.has_key(name): |
+ return (original_url, int(revision_overrides[name])) |
+ else: |
+ # TODO(aharper): SVN/SCMWrapper cleanup (non-local commandset) |
+ return (original_url, CaptureSVNHeadRevision(self._options, |
+ original_url)) |
+ else: |
+ url_components = original_url.split("@") |
+ if revision_overrides.has_key(name): |
+ return (url_components[0], int(revision_overrides[name])) |
+ else: |
+ return (url_components[0], int(url_components[1])) |
+ |
+ # Run on the base solutions first. |
+ for solution in solutions: |
+ name = solution["name"] |
+ if name in entries: |
+ raise Error("solution %s specified more than once" % name) |
+ (url, rev) = GetURLAndRev(name, solution["url"]) |
+ entries[name] = "%s@%d" % (url, rev) |
+ # TODO(aharper): SVN/SCMWrapper cleanup (non-local commandset) |
+ entries_deps_content[name] = CaptureSVN( |
+ self._options, |
+ ["cat", |
+ "%s/%s@%d" % (url, |
+ self._options.deps_file, |
+ rev)], |
+ os.getcwd()) |
+ |
+ # Process the dependencies next (sort alphanumerically to ensure that |
+ # containing directories get populated first and for readability) |
+ deps = self._ParseAllDeps(entries, entries_deps_content) |
+ deps_to_process = deps.keys() |
+ deps_to_process.sort() |
+ |
+ # First pass for direct dependencies. |
+ for d in deps_to_process: |
+ if type(deps[d]) == str: |
+ (url, rev) = GetURLAndRev(d, deps[d]) |
+ entries[d] = "%s@%d" % (url, rev) |
+ |
+ # Second pass for inherited deps (via the From keyword) |
+ for d in deps_to_process: |
+ if type(deps[d]) != str: |
+ deps_parent_url = entries[deps[d].module_name] |
+ if deps_parent_url.find("@") < 0: |
+ raise Error("From %s missing revisioned url" % deps[d].module_name) |
+ deps_parent_url_components = deps_parent_url.split("@") |
+ # TODO(aharper): SVN/SCMWrapper cleanup (non-local commandset) |
+ deps_parent_content = CaptureSVN( |
+ self._options, |
+ ["cat", |
+ "%s/%s@%s" % (deps_parent_url_components[0], |
+ self._options.deps_file, |
+ deps_parent_url_components[1])], |
+ os.getcwd()) |
+ sub_deps = self._ParseSolutionDeps( |
+ deps[d].module_name, |
+ FileRead(os.path.join(self._root_dir, |
+ deps[d].module_name, |
+ self._options.deps_file)), |
+ {}) |
+ (url, rev) = GetURLAndRev(d, sub_deps[d]) |
+ entries[d] = "%s@%d" % (url, rev) |
+ |
+ print ";".join(["%s,%s" % (x, entries[x]) for x in sorted(entries.keys())]) |
+ |
+ |
+## gclient commands. |
+ |
+ |
+def DoCleanup(options, args): |
+ """Handle the cleanup subcommand. |
+ |
+ Raises: |
+ Error: if client isn't configured properly. |
+ """ |
+ client = options.gclient.LoadCurrentConfig(options) |
+ if not client: |
+ raise Error("client not configured; see 'gclient config'") |
+ if options.verbose: |
+ # Print out the .gclient file. This is longer than if we just printed the |
+ # client dict, but more legible, and it might contain helpful comments. |
+ print >>options.stdout, client.ConfigContent() |
+ options.verbose = True |
+ return client.RunOnDeps('cleanup', args) |
+ |
+ |
+def DoConfig(options, args): |
+ """Handle the config subcommand. |
+ |
+ Args: |
+ options: If options.spec set, a string providing contents of config file. |
+ args: The command line args. If spec is not set, |
+ then args[0] is a string URL to get for config file. |
+ |
+ Raises: |
+ Error: on usage error |
+ """ |
+ if len(args) < 1 and not options.spec: |
+ raise Error("required argument missing; see 'gclient help config'") |
+ if options.path_exists(options.config_filename): |
+ raise Error("%s file already exists in the current directory" % |
+ options.config_filename) |
+ client = options.gclient('.', options) |
+ if options.spec: |
+ client.SetConfig(options.spec) |
+ else: |
+ # TODO(darin): it would be nice to be able to specify an alternate relpath |
+ # for the given URL. |
+ base_url = args[0] |
+ name = args[0].split("/")[-1] |
+ safesync_url = "" |
+ if len(args) > 1: |
+ safesync_url = args[1] |
+ client.SetDefaultConfig(name, base_url, safesync_url) |
+ client.SaveConfig() |
+ |
+ |
+def DoHelp(options, args): |
+ """Handle the help subcommand giving help for another subcommand. |
+ |
+ Raises: |
+ Error: if the command is unknown. |
+ """ |
+ if len(args) == 1 and args[0] in COMMAND_USAGE_TEXT: |
+ print >>options.stdout, COMMAND_USAGE_TEXT[args[0]] |
+ else: |
+ raise Error("unknown subcommand '%s'; see 'gclient help'" % args[0]) |
+ |
+ |
+def DoStatus(options, args): |
+ """Handle the status subcommand. |
+ |
+ Raises: |
+ Error: if client isn't configured properly. |
+ """ |
+ client = options.gclient.LoadCurrentConfig(options) |
+ if not client: |
+ raise Error("client not configured; see 'gclient config'") |
+ if options.verbose: |
+ # Print out the .gclient file. This is longer than if we just printed the |
+ # client dict, but more legible, and it might contain helpful comments. |
+ print >>options.stdout, client.ConfigContent() |
+ options.verbose = True |
+ return client.RunOnDeps('status', args) |
+ |
+ |
+def DoUpdate(options, args): |
+ """Handle the update and sync subcommands. |
+ |
+ Raises: |
+ Error: if client isn't configured properly. |
+ """ |
+ client = options.gclient.LoadCurrentConfig(options) |
+ |
+ if not client: |
+ raise Error("client not configured; see 'gclient config'") |
+ |
+ if not options.head: |
+ solutions = client.GetVar('solutions') |
+ if solutions: |
+ for s in solutions: |
+ if s.get('safesync_url', ''): |
+ # rip through revisions and make sure we're not over-riding |
+ # something that was explicitly passed |
+ has_key = False |
+ for r in options.revisions: |
+ if r.split('@')[0] == s['name']: |
+ has_key = True |
+ break |
+ |
+ if not has_key: |
+ handle = urllib.urlopen(s['safesync_url']) |
+ rev = handle.read().strip() |
+ handle.close() |
+ if len(rev): |
+ options.revisions.append(s['name']+'@'+rev) |
+ |
+ if options.verbose: |
+ # Print out the .gclient file. This is longer than if we just printed the |
+ # client dict, but more legible, and it might contain helpful comments. |
+ print >>options.stdout, client.ConfigContent() |
+ return client.RunOnDeps('update', args) |
+ |
+ |
+def DoDiff(options, args): |
+ """Handle the diff subcommand. |
+ |
+ Raises: |
+ Error: if client isn't configured properly. |
+ """ |
+ client = options.gclient.LoadCurrentConfig(options) |
+ if not client: |
+ raise Error("client not configured; see 'gclient config'") |
+ if options.verbose: |
+ # Print out the .gclient file. This is longer than if we just printed the |
+ # client dict, but more legible, and it might contain helpful comments. |
+ print >>options.stdout, client.ConfigContent() |
+ options.verbose = True |
+ return client.RunOnDeps('diff', args) |
+ |
+ |
+def DoRevert(options, args): |
+ """Handle the revert subcommand. |
+ |
+ Raises: |
+ Error: if client isn't configured properly. |
+ """ |
+ client = options.gclient.LoadCurrentConfig(options) |
+ if not client: |
+ raise Error("client not configured; see 'gclient config'") |
+ return client.RunOnDeps('revert', args) |
+ |
+ |
+def DoRunHooks(options, args): |
+ """Handle the runhooks subcommand. |
+ |
+ Raises: |
+ Error: if client isn't configured properly. |
+ """ |
+ client = options.gclient.LoadCurrentConfig(options) |
+ if not client: |
+ raise Error("client not configured; see 'gclient config'") |
+ if options.verbose: |
+ # Print out the .gclient file. This is longer than if we just printed the |
+ # client dict, but more legible, and it might contain helpful comments. |
+ print >>options.stdout, client.ConfigContent() |
+ return client.RunOnDeps('runhooks', args) |
+ |
+ |
+def DoRevInfo(options, args): |
+ """Handle the revinfo subcommand. |
+ |
+ Raises: |
+ Error: if client isn't configured properly. |
+ """ |
+ client = options.gclient.LoadCurrentConfig(options) |
+ if not client: |
+ raise Error("client not configured; see 'gclient config'") |
+ client.PrintRevInfo() |
+ |
+ |
+gclient_command_map = { |
+ "cleanup": DoCleanup, |
+ "config": DoConfig, |
+ "diff": DoDiff, |
+ "help": DoHelp, |
+ "status": DoStatus, |
+ "sync": DoUpdate, |
+ "update": DoUpdate, |
+ "revert": DoRevert, |
+ "runhooks": DoRunHooks, |
+ "revinfo" : DoRevInfo, |
+} |
+ |
+ |
+def DispatchCommand(command, options, args, command_map=None): |
+ """Dispatches the appropriate subcommand based on command line arguments.""" |
+ if command_map is None: |
+ command_map = gclient_command_map |
+ |
+ if command in command_map: |
+ return command_map[command](options, args) |
+ else: |
+ raise Error("unknown subcommand '%s'; see 'gclient help'" % command) |
+ |
+ |
+def Main(argv): |
+ """Parse command line arguments and dispatch command.""" |
+ |
+ option_parser = optparse.OptionParser(usage=DEFAULT_USAGE_TEXT, |
+ version=__version__) |
+ option_parser.disable_interspersed_args() |
+ option_parser.add_option("", "--force", action="store_true", default=False, |
+ help=("(update/sync only) force update even " |
+ "for modules which haven't changed")) |
+ option_parser.add_option("", "--revision", action="append", dest="revisions", |
+ metavar="REV", default=[], |
+ help=("(update/sync only) sync to a specific " |
+ "revision, can be used multiple times for " |
+ "each solution, e.g. --revision=src@123, " |
+ "--revision=internal@32")) |
+ option_parser.add_option("", "--deps", default=None, dest="deps_os", |
+ metavar="OS_LIST", |
+ help=("(update/sync only) sync deps for the " |
+ "specified (comma-separated) platform(s); " |
+ "'all' will sync all platforms")) |
+ option_parser.add_option("", "--spec", default=None, |
+ help=("(config only) create a gclient file " |
+ "containing the provided string")) |
+ option_parser.add_option("", "--verbose", action="store_true", default=False, |
+ help="produce additional output for diagnostics") |
+ option_parser.add_option("", "--manually_grab_svn_rev", action="store_true", |
+ default=False, |
+ help="Skip svn up whenever possible by requesting " |
+ "actual HEAD revision from the repository") |
+ option_parser.add_option("", "--head", action="store_true", default=False, |
+ help=("skips any safesync_urls specified in " |
+ "configured solutions")) |
+ |
+ if len(argv) < 2: |
+ # Users don't need to be told to use the 'help' command. |
+ option_parser.print_help() |
+ return 1 |
+ # Add manual support for --version as first argument. |
+ if argv[1] == '--version': |
+ option_parser.print_version() |
+ return 0 |
+ |
+ # Add manual support for --help as first argument. |
+ if argv[1] == '--help': |
+ argv[1] = 'help' |
+ |
+ command = argv[1] |
+ options, args = option_parser.parse_args(argv[2:]) |
+ |
+ if len(argv) < 3 and command == "help": |
+ option_parser.print_help() |
+ return 0 |
+ |
+ # Files used for configuration and state saving. |
+ options.config_filename = os.environ.get("GCLIENT_FILE", ".gclient") |
+ options.entries_filename = ".gclient_entries" |
+ options.deps_file = "DEPS" |
+ |
+ # These are overridded when testing. They are not externally visible. |
+ options.stdout = sys.stdout |
+ options.path_exists = os.path.exists |
+ options.gclient = GClient |
+ options.scm_wrapper = SCMWrapper |
+ options.platform = sys.platform |
+ return DispatchCommand(command, options, args) |
+ |
+ |
+if "__main__" == __name__: |
+ try: |
+ result = Main(sys.argv) |
+ except Error, e: |
+ print "Error: %s" % str(e) |
+ result = 1 |
+ sys.exit(result) |
+ |
+# vim: ts=2:sw=2:tw=80:et: |
Property changes on: depot_tools\gclient.py |
___________________________________________________________________ |
Added: svn:executable |
+ * |
Added: svn:eol-style |
+ LF |