blob: ef893d6d450dbd1b269f491c486d95efca1d3c73 [file] [log] [blame]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001#!/usr/bin/python
maruel@chromium.orgba551772010-02-03 18:21:42 +00002# Copyright (c) 2010 The Chromium Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00005
6"""A wrapper script to manage a set of client modules in different SCM.
7
8This script is intended to be used to help basic management of client
msb@chromium.orgd6504212010-01-13 17:34:31 +00009program sources residing in one or more Subversion modules and Git
10repositories, along with other modules it depends on, also in Subversion or Git,
11but possibly on multiple respositories, making a wrapper system apparently
12necessary.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000013
14Files
15 .gclient : Current client configuration, written by 'config' command.
16 Format is a Python script defining 'solutions', a list whose
17 entries each are maps binding the strings "name" and "url"
18 to strings specifying the name and location of the client
19 module, as well as "custom_deps" to a map similar to the DEPS
20 file below.
21 .gclient_entries : A cache constructed by 'update' command. Format is a
22 Python script defining 'entries', a list of the names
23 of all modules in the client
24 <module>/DEPS : Python script defining var 'deps' as a map from each requisite
25 submodule name to a URL where it can be found (via one SCM)
26
27Hooks
28 .gclient and DEPS files may optionally contain a list named "hooks" to
29 allow custom actions to be performed based on files that have changed in the
evan@chromium.org67820ef2009-07-27 17:23:00 +000030 working copy as a result of a "sync"/"update" or "revert" operation. This
31 could be prevented by using --nohooks (hooks run by default). Hooks can also
maruel@chromium.org5df6a462009-08-28 18:52:26 +000032 be forced to run with the "runhooks" operation. If "sync" is run with
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000033 --force, all known hooks will run regardless of the state of the working
34 copy.
35
36 Each item in a "hooks" list is a dict, containing these two keys:
37 "pattern" The associated value is a string containing a regular
38 expression. When a file whose pathname matches the expression
39 is checked out, updated, or reverted, the hook's "action" will
40 run.
41 "action" A list describing a command to run along with its arguments, if
42 any. An action command will run at most one time per gclient
43 invocation, regardless of how many files matched the pattern.
44 The action is executed in the same directory as the .gclient
45 file. If the first item in the list is the string "python",
46 the current Python interpreter (sys.executable) will be used
phajdan.jr@chromium.org71b40682009-07-31 23:40:09 +000047 to run the command. If the list contains string "$matching_files"
48 it will be removed from the list and the list will be extended
49 by the list of matching files.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000050
51 Example:
52 hooks = [
53 { "pattern": "\\.(gif|jpe?g|pr0n|png)$",
54 "action": ["python", "image_indexer.py", "--all"]},
55 ]
56"""
57
58__author__ = "darinf@gmail.com (Darin Fisher)"
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000059__version__ = "0.3.4"
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000060
61import errno
maruel@chromium.org754960e2009-09-21 12:31:05 +000062import logging
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000063import optparse
64import os
msb@chromium.org2e38de72009-09-28 17:04:47 +000065import pprint
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000066import re
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000067import sys
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000068import urlparse
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000069import urllib
70
maruel@chromium.orgada4c652009-12-03 15:32:01 +000071import breakpad
72
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000073import gclient_scm
74import gclient_utils
nasser@codeaurora.org1f7a3d12010-02-04 15:11:50 +000075from third_party.repo.progress import Progress
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000076
77# default help text
78DEFAULT_USAGE_TEXT = (
msb@chromium.orgd6504212010-01-13 17:34:31 +000079"""usage: %prog <subcommand> [options] [--] [SCM options/args...]
80a wrapper for managing a set of svn client modules and/or git repositories.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000081Version """ + __version__ + """
82
83subcommands:
84 cleanup
85 config
86 diff
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +000087 export
kbr@google.comab318592009-09-04 00:54:55 +000088 pack
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000089 revert
90 status
91 sync
92 update
93 runhooks
94 revinfo
95
msb@chromium.orgd6504212010-01-13 17:34:31 +000096Options and extra arguments can be passed to invoked SCM commands by
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000097appending them to the command line. Note that if the first such
98appended option starts with a dash (-) then the options must be
99preceded by -- to distinguish them from gclient options.
100
101For additional help on a subcommand or examples of usage, try
102 %prog help <subcommand>
103 %prog help files
104""")
105
106GENERIC_UPDATE_USAGE_TEXT = (
107 """Perform a checkout/update of the modules specified by the gclient
108configuration; see 'help config'. Unless --revision is specified,
109then the latest revision of the root solutions is checked out, with
110dependent submodule versions updated according to DEPS files.
111If --revision is specified, then the given revision is used in place
112of the latest, either for a single solution or for all solutions.
113Unless the --force option is provided, solutions and modules whose
114local revision matches the one to update (i.e., they have not changed
evan@chromium.org67820ef2009-07-27 17:23:00 +0000115in the repository) are *not* modified. Unless --nohooks is provided,
116the hooks are run.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000117This a synonym for 'gclient %(alias)s'
118
msb@chromium.orgd6504212010-01-13 17:34:31 +0000119usage: gclient %(cmd)s [options] [--] [SCM update options/args]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000120
121Valid options:
estade@chromium.org3b5cba42009-12-01 00:37:08 +0000122 --force : force update even for unchanged modules
123 --nohooks : don't run the hooks after the update is complete
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000124 --revision SOLUTION@REV : update given solution to specified revision
estade@chromium.org3b5cba42009-12-01 00:37:08 +0000125 --deps PLATFORM(S) : sync deps for the given platform(s), or 'all'
126 --verbose : output additional diagnostics
127 --head : update to latest revision, instead of last good revision
davemoore@chromium.org793796d2010-02-19 17:27:41 +0000128 --reset : resets any local changes before updating (git only)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000129
130Examples:
131 gclient %(cmd)s
msb@chromium.orgd6504212010-01-13 17:34:31 +0000132 update files from SCM according to current configuration,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000133 *for modules which have changed since last update or sync*
134 gclient %(cmd)s --force
msb@chromium.orgd6504212010-01-13 17:34:31 +0000135 update files from SCM according to current configuration, for
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000136 all modules (useful for recovering files deleted from local copy)
estade@chromium.org3b5cba42009-12-01 00:37:08 +0000137 gclient %(cmd)s --revision src@31000
138 update src directory to r31000
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000139""")
140
141COMMAND_USAGE_TEXT = {
142 "cleanup":
143 """Clean up all working copies, using 'svn cleanup' for each module.
144Additional options and args may be passed to 'svn cleanup'.
145
146usage: cleanup [options] [--] [svn cleanup args/options]
147
148Valid options:
149 --verbose : output additional diagnostics
150""",
151 "config": """Create a .gclient file in the current directory; this
152specifies the configuration for further commands. After update/sync,
153top-level DEPS files in each module are read to determine dependent
154modules to operate on as well. If optional [url] parameter is
155provided, then configuration is read from a specified Subversion server
156URL. Otherwise, a --spec option must be provided.
157
158usage: config [option | url] [safesync url]
159
160Valid options:
161 --spec=GCLIENT_SPEC : contents of .gclient are read from string parameter.
162 *Note that due to Cygwin/Python brokenness, it
163 probably can't contain any newlines.*
164
165Examples:
166 gclient config https://gclient.googlecode.com/svn/trunk/gclient
167 configure a new client to check out gclient.py tool sources
168 gclient config --spec='solutions=[{"name":"gclient","""
169 '"url":"https://gclient.googlecode.com/svn/trunk/gclient",'
170 '"custom_deps":{}}]',
171 "diff": """Display the differences between two revisions of modules.
172(Does 'svn diff' for each checked out module and dependences.)
173Additional args and options to 'svn diff' can be passed after
174gclient options.
175
176usage: diff [options] [--] [svn args/options]
177
178Valid options:
179 --verbose : output additional diagnostics
180
181Examples:
182 gclient diff
183 simple 'svn diff' for configured client and dependences
184 gclient diff -- -x -b
185 use 'svn diff -x -b' to suppress whitespace-only differences
186 gclient diff -- -r HEAD -x -b
187 diff versus the latest version of each module
188""",
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +0000189 "export":
190 """Wrapper for svn export for all managed directories
191""",
kbr@google.comab318592009-09-04 00:54:55 +0000192 "pack":
193
194 """Generate a patch which can be applied at the root of the tree.
195Internally, runs 'svn diff' on each checked out module and
196dependencies, and performs minimal postprocessing of the output. The
197resulting patch is printed to stdout and can be applied to a freshly
198checked out tree via 'patch -p0 < patchfile'. Additional args and
199options to 'svn diff' can be passed after gclient options.
200
201usage: pack [options] [--] [svn args/options]
202
203Valid options:
204 --verbose : output additional diagnostics
205
206Examples:
207 gclient pack > patch.txt
208 generate simple patch for configured client and dependences
209 gclient pack -- -x -b > patch.txt
210 generate patch using 'svn diff -x -b' to suppress
211 whitespace-only differences
212 gclient pack -- -r HEAD -x -b > patch.txt
213 generate patch, diffing each file versus the latest version of
214 each module
215""",
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000216 "revert":
217 """Revert every file in every managed directory in the client view.
218
219usage: revert
220""",
221 "status":
222 """Show the status of client and dependent modules, using 'svn diff'
223for each module. Additional options and args may be passed to 'svn diff'.
224
225usage: status [options] [--] [svn diff args/options]
226
227Valid options:
228 --verbose : output additional diagnostics
evan@chromium.org67820ef2009-07-27 17:23:00 +0000229 --nohooks : don't run the hooks after the update is complete
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000230""",
231 "sync": GENERIC_UPDATE_USAGE_TEXT % {"cmd": "sync", "alias": "update"},
232 "update": GENERIC_UPDATE_USAGE_TEXT % {"cmd": "update", "alias": "sync"},
233 "help": """Describe the usage of this program or its subcommands.
234
235usage: help [options] [subcommand]
236
237Valid options:
238 --verbose : output additional diagnostics
239""",
240 "runhooks":
241 """Runs hooks for files that have been modified in the local working copy,
maruel@chromium.org5df6a462009-08-28 18:52:26 +0000242according to 'svn status'. Implies --force.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000243
244usage: runhooks [options]
245
246Valid options:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000247 --verbose : output additional diagnostics
248""",
249 "revinfo":
250 """Outputs source path, server URL and revision information for every
251dependency in all solutions (no local checkout required).
252
253usage: revinfo [options]
254""",
255}
256
gspencer@google.comdf2d5902009-09-11 22:16:21 +0000257DEFAULT_CLIENT_FILE_TEXT = ("""\
258# An element of this array (a "solution") describes a repository directory
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000259# that will be checked out into your working copy. Each solution may
260# optionally define additional dependencies (via its DEPS file) to be
261# checked out alongside the solution's directory. A solution may also
gspencer@google.comdf2d5902009-09-11 22:16:21 +0000262# specify custom dependencies (via the "custom_deps" property) that
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000263# override or augment the dependencies specified by the DEPS file.
gspencer@google.comdf2d5902009-09-11 22:16:21 +0000264# If a "safesync_url" is specified, it is assumed to reference the location of
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000265# a text file which contains nothing but the last known good SCM revision to
266# sync against. It is fetched if specified and used unless --head is passed
gspencer@google.comdf2d5902009-09-11 22:16:21 +0000267
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000268solutions = [
gspencer@google.comdf2d5902009-09-11 22:16:21 +0000269 { "name" : "%(solution_name)s",
270 "url" : "%(solution_url)s",
271 "custom_deps" : {
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000272 # To use the trunk of a component instead of what's in DEPS:
gspencer@google.comdf2d5902009-09-11 22:16:21 +0000273 #"component": "https://svnserver/component/trunk/",
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000274 # To exclude a component from your working copy:
gspencer@google.comdf2d5902009-09-11 22:16:21 +0000275 #"data/really_large_component": None,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000276 },
gspencer@google.comdf2d5902009-09-11 22:16:21 +0000277 "safesync_url": "%(safesync_url)s"
278 },
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000279]
280""")
281
282
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000283## GClient implementation.
284
285
286class GClient(object):
287 """Object that represent a gclient checkout."""
288
289 supported_commands = [
kbr@google.comab318592009-09-04 00:54:55 +0000290 'cleanup', 'diff', 'export', 'pack', 'revert', 'status', 'update',
291 'runhooks'
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000292 ]
293
294 def __init__(self, root_dir, options):
295 self._root_dir = root_dir
296 self._options = options
297 self._config_content = None
298 self._config_dict = {}
299 self._deps_hooks = []
300
301 def SetConfig(self, content):
302 self._config_dict = {}
303 self._config_content = content
skylined@chromium.orgdf0032c2009-05-29 10:43:56 +0000304 try:
305 exec(content, self._config_dict)
306 except SyntaxError, e:
307 try:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000308 __pychecker__ = 'no-objattrs'
skylined@chromium.orgdf0032c2009-05-29 10:43:56 +0000309 # Try to construct a human readable error message
310 error_message = [
311 'There is a syntax error in your configuration file.',
312 'Line #%s, character %s:' % (e.lineno, e.offset),
313 '"%s"' % re.sub(r'[\r\n]*$', '', e.text) ]
314 except:
315 # Something went wrong, re-raise the original exception
316 raise e
317 else:
318 # Raise a new exception with the human readable message:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000319 raise gclient_utils.Error('\n'.join(error_message))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000320
321 def SaveConfig(self):
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000322 gclient_utils.FileWrite(os.path.join(self._root_dir,
323 self._options.config_filename),
324 self._config_content)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000325
326 def _LoadConfig(self):
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000327 client_source = gclient_utils.FileRead(
328 os.path.join(self._root_dir, self._options.config_filename))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000329 self.SetConfig(client_source)
330
331 def ConfigContent(self):
332 return self._config_content
333
334 def GetVar(self, key, default=None):
335 return self._config_dict.get(key, default)
336
337 @staticmethod
338 def LoadCurrentConfig(options, from_dir=None):
339 """Searches for and loads a .gclient file relative to the current working
340 dir.
341
342 Returns:
343 A dict representing the contents of the .gclient file or an empty dict if
344 the .gclient file doesn't exist.
345 """
346 if not from_dir:
347 from_dir = os.curdir
348 path = os.path.realpath(from_dir)
maruel@chromium.org0329e672009-05-13 18:41:04 +0000349 while not os.path.exists(os.path.join(path, options.config_filename)):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000350 next = os.path.split(path)
351 if not next[1]:
352 return None
353 path = next[0]
maruel@chromium.org2806acc2009-05-15 12:33:34 +0000354 client = GClient(path, options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000355 client._LoadConfig()
356 return client
357
358 def SetDefaultConfig(self, solution_name, solution_url, safesync_url):
gspencer@google.comdf2d5902009-09-11 22:16:21 +0000359 self.SetConfig(DEFAULT_CLIENT_FILE_TEXT % {
360 'solution_name': solution_name,
361 'solution_url': solution_url,
362 'safesync_url' : safesync_url,
363 })
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000364
365 def _SaveEntries(self, entries):
366 """Creates a .gclient_entries file to record the list of unique checkouts.
367
368 The .gclient_entries file lives in the same directory as .gclient.
369
370 Args:
371 entries: A sequence of solution names.
372 """
msb@chromium.org2e38de72009-09-28 17:04:47 +0000373 text = "entries = \\\n" + pprint.pformat(entries, 2) + '\n'
374 file_path = os.path.join(self._root_dir, self._options.entries_filename)
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000375 gclient_utils.FileWrite(file_path, text)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000376
377 def _ReadEntries(self):
378 """Read the .gclient_entries file for the given client.
379
380 Args:
381 client: The client for which the entries file should be read.
382
383 Returns:
384 A sequence of solution names, which will be empty if there is the
385 entries file hasn't been created yet.
386 """
387 scope = {}
388 filename = os.path.join(self._root_dir, self._options.entries_filename)
maruel@chromium.org0329e672009-05-13 18:41:04 +0000389 if not os.path.exists(filename):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000390 return []
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000391 exec(gclient_utils.FileRead(filename), scope)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000392 return scope["entries"]
393
394 class FromImpl:
395 """Used to implement the From syntax."""
396
397 def __init__(self, module_name):
398 self.module_name = module_name
399
400 def __str__(self):
401 return 'From("%s")' % self.module_name
402
403 class _VarImpl:
404 def __init__(self, custom_vars, local_scope):
405 self._custom_vars = custom_vars
406 self._local_scope = local_scope
407
408 def Lookup(self, var_name):
409 """Implements the Var syntax."""
410 if var_name in self._custom_vars:
411 return self._custom_vars[var_name]
412 elif var_name in self._local_scope.get("vars", {}):
413 return self._local_scope["vars"][var_name]
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000414 raise gclient_utils.Error("Var is not defined: %s" % var_name)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000415
416 def _ParseSolutionDeps(self, solution_name, solution_deps_content,
417 custom_vars):
418 """Parses the DEPS file for the specified solution.
419
420 Args:
421 solution_name: The name of the solution to query.
422 solution_deps_content: Content of the DEPS file for the solution
423 custom_vars: A dict of vars to override any vars defined in the DEPS file.
424
425 Returns:
426 A dict mapping module names (as relative paths) to URLs or an empty
427 dict if the solution does not have a DEPS file.
428 """
429 # Skip empty
430 if not solution_deps_content:
431 return {}
432 # Eval the content
433 local_scope = {}
434 var = self._VarImpl(custom_vars, local_scope)
435 global_scope = {"From": self.FromImpl, "Var": var.Lookup, "deps_os": {}}
436 exec(solution_deps_content, global_scope, local_scope)
437 deps = local_scope.get("deps", {})
438
439 # load os specific dependencies if defined. these dependencies may
440 # override or extend the values defined by the 'deps' member.
441 if "deps_os" in local_scope:
442 deps_os_choices = {
443 "win32": "win",
444 "win": "win",
445 "cygwin": "win",
446 "darwin": "mac",
447 "mac": "mac",
448 "unix": "unix",
449 "linux": "unix",
450 "linux2": "unix",
451 }
452
453 if self._options.deps_os is not None:
454 deps_to_include = self._options.deps_os.split(",")
455 if "all" in deps_to_include:
456 deps_to_include = deps_os_choices.values()
457 else:
458 deps_to_include = [deps_os_choices.get(self._options.platform, "unix")]
459
460 deps_to_include = set(deps_to_include)
461 for deps_os_key in deps_to_include:
462 os_deps = local_scope["deps_os"].get(deps_os_key, {})
463 if len(deps_to_include) > 1:
464 # Ignore any overrides when including deps for more than one
465 # platform, so we collect the broadest set of dependencies available.
466 # We may end up with the wrong revision of something for our
467 # platform, but this is the best we can do.
468 deps.update([x for x in os_deps.items() if not x[0] in deps])
469 else:
470 deps.update(os_deps)
471
472 if 'hooks' in local_scope:
473 self._deps_hooks.extend(local_scope['hooks'])
474
475 # If use_relative_paths is set in the DEPS file, regenerate
476 # the dictionary using paths relative to the directory containing
477 # the DEPS file.
478 if local_scope.get('use_relative_paths'):
479 rel_deps = {}
480 for d, url in deps.items():
481 # normpath is required to allow DEPS to use .. in their
482 # dependency local path.
483 rel_deps[os.path.normpath(os.path.join(solution_name, d))] = url
484 return rel_deps
485 else:
486 return deps
487
488 def _ParseAllDeps(self, solution_urls, solution_deps_content):
489 """Parse the complete list of dependencies for the client.
490
491 Args:
492 solution_urls: A dict mapping module names (as relative paths) to URLs
493 corresponding to the solutions specified by the client. This parameter
494 is passed as an optimization.
495 solution_deps_content: A dict mapping module names to the content
496 of their DEPS files
497
498 Returns:
499 A dict mapping module names (as relative paths) to URLs corresponding
500 to the entire set of dependencies to checkout for the given client.
501
502 Raises:
503 Error: If a dependency conflicts with another dependency or of a solution.
504 """
505 deps = {}
506 for solution in self.GetVar("solutions"):
507 custom_vars = solution.get("custom_vars", {})
508 solution_deps = self._ParseSolutionDeps(
509 solution["name"],
510 solution_deps_content[solution["name"]],
511 custom_vars)
512
513 # If a line is in custom_deps, but not in the solution, we want to append
514 # this line to the solution.
515 if "custom_deps" in solution:
516 for d in solution["custom_deps"]:
517 if d not in solution_deps:
518 solution_deps[d] = solution["custom_deps"][d]
519
520 for d in solution_deps:
521 if "custom_deps" in solution and d in solution["custom_deps"]:
522 # Dependency is overriden.
523 url = solution["custom_deps"][d]
524 if url is None:
525 continue
526 else:
527 url = solution_deps[d]
528 # if we have a From reference dependent on another solution, then
529 # just skip the From reference. When we pull deps for the solution,
530 # we will take care of this dependency.
531 #
532 # If multiple solutions all have the same From reference, then we
533 # should only add one to our list of dependencies.
534 if type(url) != str:
535 if url.module_name in solution_urls:
536 # Already parsed.
537 continue
538 if d in deps and type(deps[d]) != str:
539 if url.module_name == deps[d].module_name:
540 continue
541 else:
542 parsed_url = urlparse.urlparse(url)
543 scheme = parsed_url[0]
544 if not scheme:
545 # A relative url. Fetch the real base.
546 path = parsed_url[2]
547 if path[0] != "/":
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000548 raise gclient_utils.Error(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000549 "relative DEPS entry \"%s\" must begin with a slash" % d)
msb@chromium.orge6f78352010-01-13 17:05:33 +0000550 # Create a scm just to query the full url.
551 scm = gclient_scm.CreateSCM(solution["url"], self._root_dir,
552 None)
553 url = scm.FullUrlForRelativeUrl(url)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000554 if d in deps and deps[d] != url:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000555 raise gclient_utils.Error(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000556 "Solutions have conflicting versions of dependency \"%s\"" % d)
557 if d in solution_urls and solution_urls[d] != url:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000558 raise gclient_utils.Error(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000559 "Dependency \"%s\" conflicts with specified solution" % d)
560 # Grab the dependency.
561 deps[d] = url
562 return deps
563
phajdan.jr@chromium.org71b40682009-07-31 23:40:09 +0000564 def _RunHookAction(self, hook_dict, matching_file_list):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000565 """Runs the action from a single hook.
566 """
567 command = hook_dict['action'][:]
568 if command[0] == 'python':
569 # If the hook specified "python" as the first item, the action is a
570 # Python script. Run it by starting a new copy of the same
571 # interpreter.
572 command[0] = sys.executable
573
phajdan.jr@chromium.org71b40682009-07-31 23:40:09 +0000574 if '$matching_files' in command:
phajdan.jr@chromium.org68f2e092009-08-06 17:05:35 +0000575 splice_index = command.index('$matching_files')
576 command[splice_index:splice_index + 1] = matching_file_list
phajdan.jr@chromium.org71b40682009-07-31 23:40:09 +0000577
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000578 # Use a discrete exit status code of 2 to indicate that a hook action
579 # failed. Users of this script may wish to treat hook action failures
580 # differently from VC failures.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000581 gclient_utils.SubprocessCall(command, self._root_dir, fail_status=2)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000582
583 def _RunHooks(self, command, file_list, is_using_git):
584 """Evaluates all hooks, running actions as needed.
585 """
586 # Hooks only run for these command types.
587 if not command in ('update', 'revert', 'runhooks'):
588 return
589
evan@chromium.org67820ef2009-07-27 17:23:00 +0000590 # Hooks only run when --nohooks is not specified
591 if self._options.nohooks:
592 return
593
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000594 # Get any hooks from the .gclient file.
595 hooks = self.GetVar("hooks", [])
596 # Add any hooks found in DEPS files.
597 hooks.extend(self._deps_hooks)
598
599 # If "--force" was specified, run all hooks regardless of what files have
600 # changed. If the user is using git, then we don't know what files have
601 # changed so we always run all hooks.
602 if self._options.force or is_using_git:
603 for hook_dict in hooks:
phajdan.jr@chromium.org71b40682009-07-31 23:40:09 +0000604 self._RunHookAction(hook_dict, [])
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000605 return
606
607 # Run hooks on the basis of whether the files from the gclient operation
608 # match each hook's pattern.
609 for hook_dict in hooks:
610 pattern = re.compile(hook_dict['pattern'])
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000611 matching_file_list = [f for f in file_list if pattern.search(f)]
phajdan.jr@chromium.org71b40682009-07-31 23:40:09 +0000612 if matching_file_list:
613 self._RunHookAction(hook_dict, matching_file_list)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000614
615 def RunOnDeps(self, command, args):
616 """Runs a command on each dependency in a client and its dependencies.
617
618 The module's dependencies are specified in its top-level DEPS files.
619
620 Args:
621 command: The command to use (e.g., 'status' or 'diff')
622 args: list of str - extra arguments to add to the command line.
623
624 Raises:
625 Error: If the client has conflicting entries.
626 """
627 if not command in self.supported_commands:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000628 raise gclient_utils.Error("'%s' is an unsupported command" % command)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000629
630 # Check for revision overrides.
631 revision_overrides = {}
632 for revision in self._options.revisions:
633 if revision.find("@") == -1:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000634 raise gclient_utils.Error(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000635 "Specify the full dependency when specifying a revision number.")
636 revision_elem = revision.split("@")
637 # Disallow conflicting revs
638 if revision_overrides.has_key(revision_elem[0]) and \
639 revision_overrides[revision_elem[0]] != revision_elem[1]:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000640 raise gclient_utils.Error(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000641 "Conflicting revision numbers specified.")
642 revision_overrides[revision_elem[0]] = revision_elem[1]
643
644 solutions = self.GetVar("solutions")
645 if not solutions:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000646 raise gclient_utils.Error("No solution specified")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000647
648 # When running runhooks --force, there's no need to consult the SCM.
649 # All known hooks are expected to run unconditionally regardless of working
650 # copy state, so skip the SCM status check.
651 run_scm = not (command == 'runhooks' and self._options.force)
652
653 entries = {}
654 entries_deps_content = {}
655 file_list = []
656 # Run on the base solutions first.
657 for solution in solutions:
658 name = solution["name"]
gspencer@google.comdf2d5902009-09-11 22:16:21 +0000659 deps_file = solution.get("deps_file", self._options.deps_file)
660 if '/' in deps_file or '\\' in deps_file:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000661 raise gclient_utils.Error('deps_file name must not be a path, just a '
662 'filename.')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000663 if name in entries:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000664 raise gclient_utils.Error("solution %s specified more than once" % name)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000665 url = solution["url"]
666 entries[name] = url
yaar@chromium.orgf1328042009-09-22 23:14:23 +0000667 if run_scm and url:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000668 self._options.revision = revision_overrides.get(name)
msb@chromium.orgcb5442b2009-09-22 16:51:24 +0000669 scm = gclient_scm.CreateSCM(url, self._root_dir, name)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000670 scm.RunCommand(command, self._options, args, file_list)
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000671 file_list = [os.path.join(name, f.strip()) for f in file_list]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000672 self._options.revision = None
673 try:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000674 deps_content = gclient_utils.FileRead(
675 os.path.join(self._root_dir, name, deps_file))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000676 except IOError, e:
677 if e.errno != errno.ENOENT:
678 raise
679 deps_content = ""
680 entries_deps_content[name] = deps_content
681
682 # Process the dependencies next (sort alphanumerically to ensure that
683 # containing directories get populated first and for readability)
684 deps = self._ParseAllDeps(entries, entries_deps_content)
685 deps_to_process = deps.keys()
686 deps_to_process.sort()
687
688 # First pass for direct dependencies.
nasser@codeaurora.org1f7a3d12010-02-04 15:11:50 +0000689 if command == 'update' and not self._options.verbose:
690 pm = Progress('Syncing projects', len(deps_to_process))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000691 for d in deps_to_process:
nasser@codeaurora.org1f7a3d12010-02-04 15:11:50 +0000692 if command == 'update' and not self._options.verbose:
693 pm.update()
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000694 if type(deps[d]) == str:
695 url = deps[d]
696 entries[d] = url
697 if run_scm:
698 self._options.revision = revision_overrides.get(d)
msb@chromium.orgcb5442b2009-09-22 16:51:24 +0000699 scm = gclient_scm.CreateSCM(url, self._root_dir, d)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000700 scm.RunCommand(command, self._options, args, file_list)
701 self._options.revision = None
nasser@codeaurora.org1f7a3d12010-02-04 15:11:50 +0000702 if command == 'update' and not self._options.verbose:
703 pm.end()
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000704
705 # Second pass for inherited deps (via the From keyword)
706 for d in deps_to_process:
707 if type(deps[d]) != str:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000708 filename = os.path.join(self._root_dir,
709 deps[d].module_name,
710 self._options.deps_file)
711 content = gclient_utils.FileRead(filename)
712 sub_deps = self._ParseSolutionDeps(deps[d].module_name, content, {})
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000713 url = sub_deps[d]
714 entries[d] = url
715 if run_scm:
716 self._options.revision = revision_overrides.get(d)
msb@chromium.orgcb5442b2009-09-22 16:51:24 +0000717 scm = gclient_scm.CreateSCM(url, self._root_dir, d)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000718 scm.RunCommand(command, self._options, args, file_list)
719 self._options.revision = None
gspencer@google.comdf2d5902009-09-11 22:16:21 +0000720
phajdan.jr@chromium.orgd83b2b22009-08-11 15:30:55 +0000721 # Convert all absolute paths to relative.
722 for i in range(len(file_list)):
723 # TODO(phajdan.jr): We should know exactly when the paths are absolute.
724 # It depends on the command being executed (like runhooks vs sync).
725 if not os.path.isabs(file_list[i]):
726 continue
727
728 prefix = os.path.commonprefix([self._root_dir.lower(),
729 file_list[i].lower()])
730 file_list[i] = file_list[i][len(prefix):]
731
732 # Strip any leading path separators.
733 while file_list[i].startswith('\\') or file_list[i].startswith('/'):
734 file_list[i] = file_list[i][1:]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000735
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000736 is_using_git = gclient_utils.IsUsingGit(self._root_dir, entries.keys())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000737 self._RunHooks(command, file_list, is_using_git)
738
739 if command == 'update':
ajwong@chromium.orgcdcee802009-06-23 15:30:42 +0000740 # Notify the user if there is an orphaned entry in their working copy.
741 # Only delete the directory if there are no changes in it, and
742 # delete_unversioned_trees is set to true.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000743 prev_entries = self._ReadEntries()
744 for entry in prev_entries:
maruel@chromium.orgc5e9aec2009-08-03 18:25:56 +0000745 # Fix path separator on Windows.
746 entry_fixed = entry.replace('/', os.path.sep)
747 e_dir = os.path.join(self._root_dir, entry_fixed)
748 # Use entry and not entry_fixed there.
maruel@chromium.org0329e672009-05-13 18:41:04 +0000749 if entry not in entries and os.path.exists(e_dir):
msb@chromium.org83017012009-09-28 18:52:12 +0000750 modified_files = False
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000751 if isinstance(prev_entries, list):
msb@chromium.org83017012009-09-28 18:52:12 +0000752 # old .gclient_entries format was list, now dict
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000753 modified_files = gclient_scm.scm.SVN.CaptureStatus(e_dir)
msb@chromium.org83017012009-09-28 18:52:12 +0000754 else:
755 file_list = []
756 scm = gclient_scm.CreateSCM(prev_entries[entry], self._root_dir,
757 entry_fixed)
758 scm.status(self._options, [], file_list)
759 modified_files = file_list != []
760 if not self._options.delete_unversioned_trees or modified_files:
maruel@chromium.orgc5e9aec2009-08-03 18:25:56 +0000761 # There are modified files in this entry. Keep warning until
762 # removed.
maruel@chromium.orgc5e9aec2009-08-03 18:25:56 +0000763 print(("\nWARNING: \"%s\" is no longer part of this client. "
764 "It is recommended that you manually remove it.\n") %
765 entry_fixed)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000766 else:
767 # Delete the entry
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000768 print("\n________ deleting \'%s\' " +
maruel@chromium.orgc5e9aec2009-08-03 18:25:56 +0000769 "in \'%s\'") % (entry_fixed, self._root_dir)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000770 gclient_utils.RemoveDirectory(e_dir)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000771 # record the current list of entries for next time
772 self._SaveEntries(entries)
773
774 def PrintRevInfo(self):
775 """Output revision info mapping for the client and its dependencies. This
nasser@codeaurora.org952d7c72010-03-01 20:41:01 +0000776 allows the capture of an overall "revision" for the source tree that can
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000777 be used to reproduce the same tree in the future. The actual output
778 contains enough information (source paths, svn server urls and revisions)
779 that it can be used either to generate external svn commands (without
780 gclient) or as input to gclient's --rev option (with some massaging of
781 the data).
782
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000783 Raises:
784 Error: If the client has conflicting entries.
785 """
786 # Check for revision overrides.
787 revision_overrides = {}
788 for revision in self._options.revisions:
789 if revision.find("@") < 0:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000790 raise gclient_utils.Error(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000791 "Specify the full dependency when specifying a revision number.")
792 revision_elem = revision.split("@")
793 # Disallow conflicting revs
794 if revision_overrides.has_key(revision_elem[0]) and \
795 revision_overrides[revision_elem[0]] != revision_elem[1]:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000796 raise gclient_utils.Error(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000797 "Conflicting revision numbers specified.")
798 revision_overrides[revision_elem[0]] = revision_elem[1]
799
800 solutions = self.GetVar("solutions")
801 if not solutions:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000802 raise gclient_utils.Error("No solution specified")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000803
804 entries = {}
805 entries_deps_content = {}
806
807 # Inner helper to generate base url and rev tuple (including honoring
808 # |revision_overrides|)
809 def GetURLAndRev(name, original_url):
msb@chromium.orgd5a035e2009-11-13 17:58:26 +0000810 url, revision = gclient_utils.SplitUrlRevision(original_url)
msb@chromium.orgac915bb2009-11-13 17:03:01 +0000811 if not revision:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000812 if revision_overrides.has_key(name):
msb@chromium.orgac915bb2009-11-13 17:03:01 +0000813 return (url, revision_overrides[name])
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000814 else:
msb@chromium.org0f282062009-11-06 20:14:02 +0000815 scm = gclient_scm.CreateSCM(solution["url"], self._root_dir, name)
msb@chromium.orgac915bb2009-11-13 17:03:01 +0000816 return (url, scm.revinfo(self._options, [], None))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000817 else:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000818 if revision_overrides.has_key(name):
msb@chromium.orgac915bb2009-11-13 17:03:01 +0000819 return (url, revision_overrides[name])
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000820 else:
msb@chromium.orgac915bb2009-11-13 17:03:01 +0000821 return (url, revision)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000822
823 # Run on the base solutions first.
824 for solution in solutions:
825 name = solution["name"]
826 if name in entries:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000827 raise gclient_utils.Error("solution %s specified more than once" % name)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000828 (url, rev) = GetURLAndRev(name, solution["url"])
msb@chromium.org770ff9e2009-09-23 17:18:18 +0000829 entries[name] = "%s@%s" % (url, rev)
nasser@codeaurora.org952d7c72010-03-01 20:41:01 +0000830 deps_file = solution.get("deps_file", self._options.deps_file)
831 if '/' in deps_file or '\\' in deps_file:
832 raise gclient_utils.Error('deps_file name must not be a path, just a '
833 'filename.')
834 try:
835 deps_content = gclient_utils.FileRead(
836 os.path.join(self._root_dir, name, deps_file))
837 except IOError, e:
838 if e.errno != errno.ENOENT:
839 raise
840 deps_content = ""
841 entries_deps_content[name] = deps_content
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000842
843 # Process the dependencies next (sort alphanumerically to ensure that
844 # containing directories get populated first and for readability)
845 deps = self._ParseAllDeps(entries, entries_deps_content)
846 deps_to_process = deps.keys()
847 deps_to_process.sort()
848
849 # First pass for direct dependencies.
850 for d in deps_to_process:
851 if type(deps[d]) == str:
852 (url, rev) = GetURLAndRev(d, deps[d])
msb@chromium.org770ff9e2009-09-23 17:18:18 +0000853 entries[d] = "%s@%s" % (url, rev)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000854
855 # Second pass for inherited deps (via the From keyword)
856 for d in deps_to_process:
857 if type(deps[d]) != str:
858 deps_parent_url = entries[deps[d].module_name]
859 if deps_parent_url.find("@") < 0:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000860 raise gclient_utils.Error("From %s missing revisioned url" %
861 deps[d].module_name)
862 content = gclient_utils.FileRead(os.path.join(self._root_dir,
863 deps[d].module_name,
864 self._options.deps_file))
865 sub_deps = self._ParseSolutionDeps(deps[d].module_name, content, {})
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000866 (url, rev) = GetURLAndRev(d, sub_deps[d])
msb@chromium.org770ff9e2009-09-23 17:18:18 +0000867 entries[d] = "%s@%s" % (url, rev)
nasser@codeaurora.org952d7c72010-03-01 20:41:01 +0000868 print(";\n".join(["%s: %s" % (x, entries[x])
maruel@chromium.org57e893e2009-08-19 18:12:09 +0000869 for x in sorted(entries.keys())]))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000870
871
872## gclient commands.
873
874
875def DoCleanup(options, args):
876 """Handle the cleanup subcommand.
877
878 Raises:
879 Error: if client isn't configured properly.
880 """
maruel@chromium.org2806acc2009-05-15 12:33:34 +0000881 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000882 if not client:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000883 raise gclient_utils.Error("client not configured; see 'gclient config'")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000884 if options.verbose:
885 # Print out the .gclient file. This is longer than if we just printed the
886 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000887 print(client.ConfigContent())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000888 return client.RunOnDeps('cleanup', args)
889
890
891def DoConfig(options, args):
892 """Handle the config subcommand.
893
894 Args:
895 options: If options.spec set, a string providing contents of config file.
896 args: The command line args. If spec is not set,
897 then args[0] is a string URL to get for config file.
898
899 Raises:
900 Error: on usage error
901 """
902 if len(args) < 1 and not options.spec:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000903 raise gclient_utils.Error("required argument missing; see 'gclient help "
904 "config'")
maruel@chromium.org0329e672009-05-13 18:41:04 +0000905 if os.path.exists(options.config_filename):
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000906 raise gclient_utils.Error("%s file already exists in the current directory"
907 % options.config_filename)
maruel@chromium.org2806acc2009-05-15 12:33:34 +0000908 client = GClient('.', options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000909 if options.spec:
910 client.SetConfig(options.spec)
911 else:
912 # TODO(darin): it would be nice to be able to specify an alternate relpath
913 # for the given URL.
maruel@chromium.org1ab7ffc2009-06-03 17:21:37 +0000914 base_url = args[0].rstrip('/')
915 name = base_url.split("/")[-1]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000916 safesync_url = ""
917 if len(args) > 1:
918 safesync_url = args[1]
919 client.SetDefaultConfig(name, base_url, safesync_url)
920 client.SaveConfig()
921
922
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +0000923def DoExport(options, args):
924 """Handle the export subcommand.
phajdan.jr@chromium.org71b40682009-07-31 23:40:09 +0000925
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +0000926 Raises:
927 Error: on usage error
928 """
929 if len(args) != 1:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000930 raise gclient_utils.Error("Need directory name")
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +0000931 client = GClient.LoadCurrentConfig(options)
932
933 if not client:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000934 raise gclient_utils.Error("client not configured; see 'gclient config'")
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +0000935
936 if options.verbose:
937 # Print out the .gclient file. This is longer than if we just printed the
938 # client dict, but more legible, and it might contain helpful comments.
939 print(client.ConfigContent())
940 return client.RunOnDeps('export', args)
941
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000942def DoHelp(options, args):
943 """Handle the help subcommand giving help for another subcommand.
944
945 Raises:
946 Error: if the command is unknown.
947 """
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000948 __pychecker__ = 'unusednames=options'
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000949 if len(args) == 1 and args[0] in COMMAND_USAGE_TEXT:
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000950 print(COMMAND_USAGE_TEXT[args[0]])
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000951 else:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000952 raise gclient_utils.Error("unknown subcommand '%s'; see 'gclient help'" %
953 args[0])
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000954
955
kbr@google.comab318592009-09-04 00:54:55 +0000956def DoPack(options, args):
957 """Handle the pack subcommand.
958
959 Raises:
960 Error: if client isn't configured properly.
961 """
962 client = GClient.LoadCurrentConfig(options)
963 if not client:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000964 raise gclient_utils.Error("client not configured; see 'gclient config'")
kbr@google.comab318592009-09-04 00:54:55 +0000965 if options.verbose:
966 # Print out the .gclient file. This is longer than if we just printed the
967 # client dict, but more legible, and it might contain helpful comments.
968 print(client.ConfigContent())
kbr@google.comab318592009-09-04 00:54:55 +0000969 return client.RunOnDeps('pack', args)
970
971
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000972def DoStatus(options, args):
973 """Handle the status subcommand.
974
975 Raises:
976 Error: if client isn't configured properly.
977 """
maruel@chromium.org2806acc2009-05-15 12:33:34 +0000978 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000979 if not client:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000980 raise gclient_utils.Error("client not configured; see 'gclient config'")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000981 if options.verbose:
982 # Print out the .gclient file. This is longer than if we just printed the
983 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000984 print(client.ConfigContent())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000985 return client.RunOnDeps('status', args)
986
987
988def DoUpdate(options, args):
989 """Handle the update and sync subcommands.
990
991 Raises:
992 Error: if client isn't configured properly.
993 """
maruel@chromium.org2806acc2009-05-15 12:33:34 +0000994 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000995
996 if not client:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000997 raise gclient_utils.Error("client not configured; see 'gclient config'")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000998
999 if not options.head:
1000 solutions = client.GetVar('solutions')
1001 if solutions:
1002 for s in solutions:
1003 if s.get('safesync_url', ''):
1004 # rip through revisions and make sure we're not over-riding
1005 # something that was explicitly passed
1006 has_key = False
1007 for r in options.revisions:
1008 if r.split('@')[0] == s['name']:
1009 has_key = True
1010 break
1011
1012 if not has_key:
1013 handle = urllib.urlopen(s['safesync_url'])
1014 rev = handle.read().strip()
1015 handle.close()
1016 if len(rev):
1017 options.revisions.append(s['name']+'@'+rev)
1018
1019 if options.verbose:
1020 # Print out the .gclient file. This is longer than if we just printed the
1021 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +00001022 print(client.ConfigContent())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001023 return client.RunOnDeps('update', args)
1024
1025
1026def DoDiff(options, args):
1027 """Handle the diff subcommand.
1028
1029 Raises:
1030 Error: if client isn't configured properly.
1031 """
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001032 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001033 if not client:
maruel@chromium.orge3608df2009-11-10 20:22:57 +00001034 raise gclient_utils.Error("client not configured; see 'gclient config'")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001035 if options.verbose:
1036 # Print out the .gclient file. This is longer than if we just printed the
1037 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +00001038 print(client.ConfigContent())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001039 return client.RunOnDeps('diff', args)
1040
1041
1042def DoRevert(options, args):
1043 """Handle the revert subcommand.
1044
1045 Raises:
1046 Error: if client isn't configured properly.
1047 """
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001048 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001049 if not client:
maruel@chromium.orge3608df2009-11-10 20:22:57 +00001050 raise gclient_utils.Error("client not configured; see 'gclient config'")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001051 return client.RunOnDeps('revert', args)
1052
1053
1054def DoRunHooks(options, args):
1055 """Handle the runhooks subcommand.
1056
1057 Raises:
1058 Error: if client isn't configured properly.
1059 """
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001060 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001061 if not client:
maruel@chromium.orge3608df2009-11-10 20:22:57 +00001062 raise gclient_utils.Error("client not configured; see 'gclient config'")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001063 if options.verbose:
1064 # Print out the .gclient file. This is longer than if we just printed the
1065 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +00001066 print(client.ConfigContent())
maruel@chromium.org5df6a462009-08-28 18:52:26 +00001067 options.force = True
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001068 return client.RunOnDeps('runhooks', args)
1069
1070
1071def DoRevInfo(options, args):
1072 """Handle the revinfo subcommand.
1073
1074 Raises:
1075 Error: if client isn't configured properly.
1076 """
maruel@chromium.orge3608df2009-11-10 20:22:57 +00001077 __pychecker__ = 'unusednames=args'
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001078 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001079 if not client:
maruel@chromium.orge3608df2009-11-10 20:22:57 +00001080 raise gclient_utils.Error("client not configured; see 'gclient config'")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001081 client.PrintRevInfo()
1082
1083
1084gclient_command_map = {
1085 "cleanup": DoCleanup,
1086 "config": DoConfig,
1087 "diff": DoDiff,
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +00001088 "export": DoExport,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001089 "help": DoHelp,
kbr@google.comab318592009-09-04 00:54:55 +00001090 "pack": DoPack,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001091 "status": DoStatus,
1092 "sync": DoUpdate,
1093 "update": DoUpdate,
1094 "revert": DoRevert,
1095 "runhooks": DoRunHooks,
1096 "revinfo" : DoRevInfo,
1097}
1098
1099
1100def DispatchCommand(command, options, args, command_map=None):
1101 """Dispatches the appropriate subcommand based on command line arguments."""
1102 if command_map is None:
1103 command_map = gclient_command_map
1104
1105 if command in command_map:
1106 return command_map[command](options, args)
1107 else:
maruel@chromium.orge3608df2009-11-10 20:22:57 +00001108 raise gclient_utils.Error("unknown subcommand '%s'; see 'gclient help'" %
1109 command)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001110
1111
1112def Main(argv):
1113 """Parse command line arguments and dispatch command."""
1114
1115 option_parser = optparse.OptionParser(usage=DEFAULT_USAGE_TEXT,
1116 version=__version__)
1117 option_parser.disable_interspersed_args()
1118 option_parser.add_option("", "--force", action="store_true", default=False,
1119 help=("(update/sync only) force update even "
1120 "for modules which haven't changed"))
evan@chromium.org67820ef2009-07-27 17:23:00 +00001121 option_parser.add_option("", "--nohooks", action="store_true", default=False,
1122 help=("(update/sync/revert only) prevent the hooks from "
1123 "running"))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001124 option_parser.add_option("", "--revision", action="append", dest="revisions",
1125 metavar="REV", default=[],
1126 help=("(update/sync only) sync to a specific "
1127 "revision, can be used multiple times for "
1128 "each solution, e.g. --revision=src@123, "
1129 "--revision=internal@32"))
1130 option_parser.add_option("", "--deps", default=None, dest="deps_os",
1131 metavar="OS_LIST",
1132 help=("(update/sync only) sync deps for the "
1133 "specified (comma-separated) platform(s); "
1134 "'all' will sync all platforms"))
davemoore@chromium.org793796d2010-02-19 17:27:41 +00001135 option_parser.add_option("", "--reset", action="store_true", default=False,
1136 help=("(update/sync only) resets any local changes "
1137 "before updating (git only)"))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001138 option_parser.add_option("", "--spec", default=None,
1139 help=("(config only) create a gclient file "
1140 "containing the provided string"))
maruel@chromium.orga6220d12010-01-06 21:04:17 +00001141 option_parser.add_option("-v", "--verbose", action="count", default=0,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001142 help="produce additional output for diagnostics")
maruel@chromium.org7753d242009-10-07 17:40:24 +00001143 option_parser.add_option("", "--manually_grab_svn_rev", action="store_true",
1144 default=False,
1145 help="Skip svn up whenever possible by requesting "
1146 "actual HEAD revision from the repository")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001147 option_parser.add_option("", "--head", action="store_true", default=False,
1148 help=("skips any safesync_urls specified in "
1149 "configured solutions"))
ajwong@chromium.orgcdcee802009-06-23 15:30:42 +00001150 option_parser.add_option("", "--delete_unversioned_trees",
1151 action="store_true", default=False,
1152 help=("on update, delete any unexpected "
1153 "unversioned trees that are in the checkout"))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001154
1155 if len(argv) < 2:
1156 # Users don't need to be told to use the 'help' command.
1157 option_parser.print_help()
1158 return 1
1159 # Add manual support for --version as first argument.
1160 if argv[1] == '--version':
1161 option_parser.print_version()
1162 return 0
1163
1164 # Add manual support for --help as first argument.
1165 if argv[1] == '--help':
1166 argv[1] = 'help'
1167
1168 command = argv[1]
1169 options, args = option_parser.parse_args(argv[2:])
1170
1171 if len(argv) < 3 and command == "help":
1172 option_parser.print_help()
1173 return 0
1174
maruel@chromium.orga6220d12010-01-06 21:04:17 +00001175 if options.verbose > 1:
maruel@chromium.org754960e2009-09-21 12:31:05 +00001176 logging.basicConfig(level=logging.DEBUG)
1177
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001178 # Files used for configuration and state saving.
1179 options.config_filename = os.environ.get("GCLIENT_FILE", ".gclient")
1180 options.entries_filename = ".gclient_entries"
1181 options.deps_file = "DEPS"
1182
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001183 options.platform = sys.platform
1184 return DispatchCommand(command, options, args)
1185
1186
1187if "__main__" == __name__:
1188 try:
1189 result = Main(sys.argv)
maruel@chromium.orge3608df2009-11-10 20:22:57 +00001190 except gclient_utils.Error, e:
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +00001191 print >> sys.stderr, "Error: %s" % str(e)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001192 result = 1
1193 sys.exit(result)
1194
1195# vim: ts=2:sw=2:tw=80:et: