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

Unified Diff: depot_tools/gclient.py

Issue 92087: Create the Next Generation of depot_tools. Eh. (Closed) Base URL: svn://chrome-svn.corp.google.com/chrome/trunk/tools/
Patch Set: Created 11 years, 8 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
« no previous file with comments | « depot_tools/gclient.bat ('k') | depot_tools/git-cl.py » ('j') | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
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
« no previous file with comments | « depot_tools/gclient.bat ('k') | depot_tools/git-cl.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698