blob: da87501c40f2a8c3e034cf1a451f69724ae5e365 [file] [log] [blame]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001#!/usr/bin/python
2#
3# Copyright 2008 Google Inc. All Rights Reserved.
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16
17"""A wrapper script to manage a set of client modules in different SCM.
18
19This script is intended to be used to help basic management of client
20program sources residing in one or more Subversion modules, along with
21other modules it depends on, also in Subversion, but possibly on
22multiple respositories, making a wrapper system apparently necessary.
23
24Files
25 .gclient : Current client configuration, written by 'config' command.
26 Format is a Python script defining 'solutions', a list whose
27 entries each are maps binding the strings "name" and "url"
28 to strings specifying the name and location of the client
29 module, as well as "custom_deps" to a map similar to the DEPS
30 file below.
31 .gclient_entries : A cache constructed by 'update' command. Format is a
32 Python script defining 'entries', a list of the names
33 of all modules in the client
34 <module>/DEPS : Python script defining var 'deps' as a map from each requisite
35 submodule name to a URL where it can be found (via one SCM)
36
37Hooks
38 .gclient and DEPS files may optionally contain a list named "hooks" to
39 allow custom actions to be performed based on files that have changed in the
evan@chromium.org67820ef2009-07-27 17:23:00 +000040 working copy as a result of a "sync"/"update" or "revert" operation. This
41 could be prevented by using --nohooks (hooks run by default). Hooks can also
maruel@chromium.org5df6a462009-08-28 18:52:26 +000042 be forced to run with the "runhooks" operation. If "sync" is run with
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000043 --force, all known hooks will run regardless of the state of the working
44 copy.
45
46 Each item in a "hooks" list is a dict, containing these two keys:
47 "pattern" The associated value is a string containing a regular
48 expression. When a file whose pathname matches the expression
49 is checked out, updated, or reverted, the hook's "action" will
50 run.
51 "action" A list describing a command to run along with its arguments, if
52 any. An action command will run at most one time per gclient
53 invocation, regardless of how many files matched the pattern.
54 The action is executed in the same directory as the .gclient
55 file. If the first item in the list is the string "python",
56 the current Python interpreter (sys.executable) will be used
phajdan.jr@chromium.org71b40682009-07-31 23:40:09 +000057 to run the command. If the list contains string "$matching_files"
58 it will be removed from the list and the list will be extended
59 by the list of matching files.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000060
61 Example:
62 hooks = [
63 { "pattern": "\\.(gif|jpe?g|pr0n|png)$",
64 "action": ["python", "image_indexer.py", "--all"]},
65 ]
66"""
67
68__author__ = "darinf@gmail.com (Darin Fisher)"
maruel@chromium.org5df6a462009-08-28 18:52:26 +000069__version__ = "0.3.3"
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000070
71import errno
maruel@chromium.org754960e2009-09-21 12:31:05 +000072import logging
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000073import optparse
74import os
msb@chromium.org2e38de72009-09-28 17:04:47 +000075import pprint
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000076import re
77import stat
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000078import sys
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000079import urlparse
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000080import urllib
81
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000082import gclient_scm
83import gclient_utils
84from gclient_utils import Error, FileRead, FileWrite
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000085
86# default help text
87DEFAULT_USAGE_TEXT = (
88"""usage: %prog <subcommand> [options] [--] [svn options/args...]
89a wrapper for managing a set of client modules in svn.
90Version """ + __version__ + """
91
92subcommands:
93 cleanup
94 config
95 diff
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +000096 export
kbr@google.comab318592009-09-04 00:54:55 +000097 pack
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000098 revert
99 status
100 sync
101 update
102 runhooks
103 revinfo
104
105Options and extra arguments can be passed to invoked svn commands by
106appending them to the command line. Note that if the first such
107appended option starts with a dash (-) then the options must be
108preceded by -- to distinguish them from gclient options.
109
110For additional help on a subcommand or examples of usage, try
111 %prog help <subcommand>
112 %prog help files
113""")
114
115GENERIC_UPDATE_USAGE_TEXT = (
116 """Perform a checkout/update of the modules specified by the gclient
117configuration; see 'help config'. Unless --revision is specified,
118then the latest revision of the root solutions is checked out, with
119dependent submodule versions updated according to DEPS files.
120If --revision is specified, then the given revision is used in place
121of the latest, either for a single solution or for all solutions.
122Unless the --force option is provided, solutions and modules whose
123local revision matches the one to update (i.e., they have not changed
evan@chromium.org67820ef2009-07-27 17:23:00 +0000124in the repository) are *not* modified. Unless --nohooks is provided,
125the hooks are run.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000126This a synonym for 'gclient %(alias)s'
127
128usage: gclient %(cmd)s [options] [--] [svn update options/args]
129
130Valid options:
131 --force : force update even for unchanged modules
evan@chromium.org67820ef2009-07-27 17:23:00 +0000132 --nohooks : don't run the hooks after the update is complete
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000133 --revision REV : update/checkout all solutions with specified revision
134 --revision SOLUTION@REV : update given solution to specified revision
135 --deps PLATFORM(S) : sync deps for the given platform(s), or 'all'
136 --verbose : output additional diagnostics
maruel@chromium.orgb8b6b872009-06-30 18:50:56 +0000137 --head : update to latest revision, instead of last good revision
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000138
139Examples:
140 gclient %(cmd)s
141 update files from SVN according to current configuration,
142 *for modules which have changed since last update or sync*
143 gclient %(cmd)s --force
144 update files from SVN according to current configuration, for
145 all modules (useful for recovering files deleted from local copy)
146""")
147
148COMMAND_USAGE_TEXT = {
149 "cleanup":
150 """Clean up all working copies, using 'svn cleanup' for each module.
151Additional options and args may be passed to 'svn cleanup'.
152
153usage: cleanup [options] [--] [svn cleanup args/options]
154
155Valid options:
156 --verbose : output additional diagnostics
157""",
158 "config": """Create a .gclient file in the current directory; this
159specifies the configuration for further commands. After update/sync,
160top-level DEPS files in each module are read to determine dependent
161modules to operate on as well. If optional [url] parameter is
162provided, then configuration is read from a specified Subversion server
163URL. Otherwise, a --spec option must be provided.
164
165usage: config [option | url] [safesync url]
166
167Valid options:
168 --spec=GCLIENT_SPEC : contents of .gclient are read from string parameter.
169 *Note that due to Cygwin/Python brokenness, it
170 probably can't contain any newlines.*
171
172Examples:
173 gclient config https://gclient.googlecode.com/svn/trunk/gclient
174 configure a new client to check out gclient.py tool sources
175 gclient config --spec='solutions=[{"name":"gclient","""
176 '"url":"https://gclient.googlecode.com/svn/trunk/gclient",'
177 '"custom_deps":{}}]',
178 "diff": """Display the differences between two revisions of modules.
179(Does 'svn diff' for each checked out module and dependences.)
180Additional args and options to 'svn diff' can be passed after
181gclient options.
182
183usage: diff [options] [--] [svn args/options]
184
185Valid options:
186 --verbose : output additional diagnostics
187
188Examples:
189 gclient diff
190 simple 'svn diff' for configured client and dependences
191 gclient diff -- -x -b
192 use 'svn diff -x -b' to suppress whitespace-only differences
193 gclient diff -- -r HEAD -x -b
194 diff versus the latest version of each module
195""",
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +0000196 "export":
197 """Wrapper for svn export for all managed directories
198""",
kbr@google.comab318592009-09-04 00:54:55 +0000199 "pack":
200
201 """Generate a patch which can be applied at the root of the tree.
202Internally, runs 'svn diff' on each checked out module and
203dependencies, and performs minimal postprocessing of the output. The
204resulting patch is printed to stdout and can be applied to a freshly
205checked out tree via 'patch -p0 < patchfile'. Additional args and
206options to 'svn diff' can be passed after gclient options.
207
208usage: pack [options] [--] [svn args/options]
209
210Valid options:
211 --verbose : output additional diagnostics
212
213Examples:
214 gclient pack > patch.txt
215 generate simple patch for configured client and dependences
216 gclient pack -- -x -b > patch.txt
217 generate patch using 'svn diff -x -b' to suppress
218 whitespace-only differences
219 gclient pack -- -r HEAD -x -b > patch.txt
220 generate patch, diffing each file versus the latest version of
221 each module
222""",
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000223 "revert":
224 """Revert every file in every managed directory in the client view.
225
226usage: revert
227""",
228 "status":
229 """Show the status of client and dependent modules, using 'svn diff'
230for each module. Additional options and args may be passed to 'svn diff'.
231
232usage: status [options] [--] [svn diff args/options]
233
234Valid options:
235 --verbose : output additional diagnostics
evan@chromium.org67820ef2009-07-27 17:23:00 +0000236 --nohooks : don't run the hooks after the update is complete
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000237""",
238 "sync": GENERIC_UPDATE_USAGE_TEXT % {"cmd": "sync", "alias": "update"},
239 "update": GENERIC_UPDATE_USAGE_TEXT % {"cmd": "update", "alias": "sync"},
240 "help": """Describe the usage of this program or its subcommands.
241
242usage: help [options] [subcommand]
243
244Valid options:
245 --verbose : output additional diagnostics
246""",
247 "runhooks":
248 """Runs hooks for files that have been modified in the local working copy,
maruel@chromium.org5df6a462009-08-28 18:52:26 +0000249according to 'svn status'. Implies --force.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000250
251usage: runhooks [options]
252
253Valid options:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000254 --verbose : output additional diagnostics
255""",
256 "revinfo":
257 """Outputs source path, server URL and revision information for every
258dependency in all solutions (no local checkout required).
259
260usage: revinfo [options]
261""",
262}
263
gspencer@google.comdf2d5902009-09-11 22:16:21 +0000264DEFAULT_CLIENT_FILE_TEXT = ("""\
265# An element of this array (a "solution") describes a repository directory
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000266# that will be checked out into your working copy. Each solution may
267# optionally define additional dependencies (via its DEPS file) to be
268# checked out alongside the solution's directory. A solution may also
gspencer@google.comdf2d5902009-09-11 22:16:21 +0000269# specify custom dependencies (via the "custom_deps" property) that
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000270# override or augment the dependencies specified by the DEPS file.
gspencer@google.comdf2d5902009-09-11 22:16:21 +0000271# If a "safesync_url" is specified, it is assumed to reference the location of
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000272# a text file which contains nothing but the last known good SCM revision to
273# sync against. It is fetched if specified and used unless --head is passed
gspencer@google.comdf2d5902009-09-11 22:16:21 +0000274
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000275solutions = [
gspencer@google.comdf2d5902009-09-11 22:16:21 +0000276 { "name" : "%(solution_name)s",
277 "url" : "%(solution_url)s",
278 "custom_deps" : {
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000279 # To use the trunk of a component instead of what's in DEPS:
gspencer@google.comdf2d5902009-09-11 22:16:21 +0000280 #"component": "https://svnserver/component/trunk/",
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000281 # To exclude a component from your working copy:
gspencer@google.comdf2d5902009-09-11 22:16:21 +0000282 #"data/really_large_component": None,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000283 },
gspencer@google.comdf2d5902009-09-11 22:16:21 +0000284 "safesync_url": "%(safesync_url)s"
285 },
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000286]
287""")
288
289
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000290## GClient implementation.
291
292
293class GClient(object):
294 """Object that represent a gclient checkout."""
295
296 supported_commands = [
kbr@google.comab318592009-09-04 00:54:55 +0000297 'cleanup', 'diff', 'export', 'pack', 'revert', 'status', 'update',
298 'runhooks'
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000299 ]
300
301 def __init__(self, root_dir, options):
302 self._root_dir = root_dir
303 self._options = options
304 self._config_content = None
305 self._config_dict = {}
306 self._deps_hooks = []
307
308 def SetConfig(self, content):
309 self._config_dict = {}
310 self._config_content = content
skylined@chromium.orgdf0032c2009-05-29 10:43:56 +0000311 try:
312 exec(content, self._config_dict)
313 except SyntaxError, e:
314 try:
315 # Try to construct a human readable error message
316 error_message = [
317 'There is a syntax error in your configuration file.',
318 'Line #%s, character %s:' % (e.lineno, e.offset),
319 '"%s"' % re.sub(r'[\r\n]*$', '', e.text) ]
320 except:
321 # Something went wrong, re-raise the original exception
322 raise e
323 else:
324 # Raise a new exception with the human readable message:
325 raise Error('\n'.join(error_message))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000326
327 def SaveConfig(self):
328 FileWrite(os.path.join(self._root_dir, self._options.config_filename),
329 self._config_content)
330
331 def _LoadConfig(self):
332 client_source = FileRead(os.path.join(self._root_dir,
333 self._options.config_filename))
334 self.SetConfig(client_source)
335
336 def ConfigContent(self):
337 return self._config_content
338
339 def GetVar(self, key, default=None):
340 return self._config_dict.get(key, default)
341
342 @staticmethod
343 def LoadCurrentConfig(options, from_dir=None):
344 """Searches for and loads a .gclient file relative to the current working
345 dir.
346
347 Returns:
348 A dict representing the contents of the .gclient file or an empty dict if
349 the .gclient file doesn't exist.
350 """
351 if not from_dir:
352 from_dir = os.curdir
353 path = os.path.realpath(from_dir)
maruel@chromium.org0329e672009-05-13 18:41:04 +0000354 while not os.path.exists(os.path.join(path, options.config_filename)):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000355 next = os.path.split(path)
356 if not next[1]:
357 return None
358 path = next[0]
maruel@chromium.org2806acc2009-05-15 12:33:34 +0000359 client = GClient(path, options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000360 client._LoadConfig()
361 return client
362
363 def SetDefaultConfig(self, solution_name, solution_url, safesync_url):
gspencer@google.comdf2d5902009-09-11 22:16:21 +0000364 self.SetConfig(DEFAULT_CLIENT_FILE_TEXT % {
365 'solution_name': solution_name,
366 'solution_url': solution_url,
367 'safesync_url' : safesync_url,
368 })
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000369
370 def _SaveEntries(self, entries):
371 """Creates a .gclient_entries file to record the list of unique checkouts.
372
373 The .gclient_entries file lives in the same directory as .gclient.
374
375 Args:
376 entries: A sequence of solution names.
377 """
msb@chromium.org2e38de72009-09-28 17:04:47 +0000378 text = "entries = \\\n" + pprint.pformat(entries, 2) + '\n'
379 file_path = os.path.join(self._root_dir, self._options.entries_filename)
380 FileWrite(file_path, text)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000381
382 def _ReadEntries(self):
383 """Read the .gclient_entries file for the given client.
384
385 Args:
386 client: The client for which the entries file should be read.
387
388 Returns:
389 A sequence of solution names, which will be empty if there is the
390 entries file hasn't been created yet.
391 """
392 scope = {}
393 filename = os.path.join(self._root_dir, self._options.entries_filename)
maruel@chromium.org0329e672009-05-13 18:41:04 +0000394 if not os.path.exists(filename):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000395 return []
396 exec(FileRead(filename), scope)
397 return scope["entries"]
398
399 class FromImpl:
400 """Used to implement the From syntax."""
401
402 def __init__(self, module_name):
403 self.module_name = module_name
404
405 def __str__(self):
406 return 'From("%s")' % self.module_name
407
408 class _VarImpl:
409 def __init__(self, custom_vars, local_scope):
410 self._custom_vars = custom_vars
411 self._local_scope = local_scope
412
413 def Lookup(self, var_name):
414 """Implements the Var syntax."""
415 if var_name in self._custom_vars:
416 return self._custom_vars[var_name]
417 elif var_name in self._local_scope.get("vars", {}):
418 return self._local_scope["vars"][var_name]
419 raise Error("Var is not defined: %s" % var_name)
420
421 def _ParseSolutionDeps(self, solution_name, solution_deps_content,
422 custom_vars):
423 """Parses the DEPS file for the specified solution.
424
425 Args:
426 solution_name: The name of the solution to query.
427 solution_deps_content: Content of the DEPS file for the solution
428 custom_vars: A dict of vars to override any vars defined in the DEPS file.
429
430 Returns:
431 A dict mapping module names (as relative paths) to URLs or an empty
432 dict if the solution does not have a DEPS file.
433 """
434 # Skip empty
435 if not solution_deps_content:
436 return {}
437 # Eval the content
438 local_scope = {}
439 var = self._VarImpl(custom_vars, local_scope)
440 global_scope = {"From": self.FromImpl, "Var": var.Lookup, "deps_os": {}}
441 exec(solution_deps_content, global_scope, local_scope)
442 deps = local_scope.get("deps", {})
443
444 # load os specific dependencies if defined. these dependencies may
445 # override or extend the values defined by the 'deps' member.
446 if "deps_os" in local_scope:
447 deps_os_choices = {
448 "win32": "win",
449 "win": "win",
450 "cygwin": "win",
451 "darwin": "mac",
452 "mac": "mac",
453 "unix": "unix",
454 "linux": "unix",
455 "linux2": "unix",
456 }
457
458 if self._options.deps_os is not None:
459 deps_to_include = self._options.deps_os.split(",")
460 if "all" in deps_to_include:
461 deps_to_include = deps_os_choices.values()
462 else:
463 deps_to_include = [deps_os_choices.get(self._options.platform, "unix")]
464
465 deps_to_include = set(deps_to_include)
466 for deps_os_key in deps_to_include:
467 os_deps = local_scope["deps_os"].get(deps_os_key, {})
468 if len(deps_to_include) > 1:
469 # Ignore any overrides when including deps for more than one
470 # platform, so we collect the broadest set of dependencies available.
471 # We may end up with the wrong revision of something for our
472 # platform, but this is the best we can do.
473 deps.update([x for x in os_deps.items() if not x[0] in deps])
474 else:
475 deps.update(os_deps)
476
477 if 'hooks' in local_scope:
478 self._deps_hooks.extend(local_scope['hooks'])
479
480 # If use_relative_paths is set in the DEPS file, regenerate
481 # the dictionary using paths relative to the directory containing
482 # the DEPS file.
483 if local_scope.get('use_relative_paths'):
484 rel_deps = {}
485 for d, url in deps.items():
486 # normpath is required to allow DEPS to use .. in their
487 # dependency local path.
488 rel_deps[os.path.normpath(os.path.join(solution_name, d))] = url
489 return rel_deps
490 else:
491 return deps
492
493 def _ParseAllDeps(self, solution_urls, solution_deps_content):
494 """Parse the complete list of dependencies for the client.
495
496 Args:
497 solution_urls: A dict mapping module names (as relative paths) to URLs
498 corresponding to the solutions specified by the client. This parameter
499 is passed as an optimization.
500 solution_deps_content: A dict mapping module names to the content
501 of their DEPS files
502
503 Returns:
504 A dict mapping module names (as relative paths) to URLs corresponding
505 to the entire set of dependencies to checkout for the given client.
506
507 Raises:
508 Error: If a dependency conflicts with another dependency or of a solution.
509 """
510 deps = {}
511 for solution in self.GetVar("solutions"):
512 custom_vars = solution.get("custom_vars", {})
513 solution_deps = self._ParseSolutionDeps(
514 solution["name"],
515 solution_deps_content[solution["name"]],
516 custom_vars)
517
518 # If a line is in custom_deps, but not in the solution, we want to append
519 # this line to the solution.
520 if "custom_deps" in solution:
521 for d in solution["custom_deps"]:
522 if d not in solution_deps:
523 solution_deps[d] = solution["custom_deps"][d]
524
525 for d in solution_deps:
526 if "custom_deps" in solution and d in solution["custom_deps"]:
527 # Dependency is overriden.
528 url = solution["custom_deps"][d]
529 if url is None:
530 continue
531 else:
532 url = solution_deps[d]
533 # if we have a From reference dependent on another solution, then
534 # just skip the From reference. When we pull deps for the solution,
535 # we will take care of this dependency.
536 #
537 # If multiple solutions all have the same From reference, then we
538 # should only add one to our list of dependencies.
539 if type(url) != str:
540 if url.module_name in solution_urls:
541 # Already parsed.
542 continue
543 if d in deps and type(deps[d]) != str:
544 if url.module_name == deps[d].module_name:
545 continue
546 else:
547 parsed_url = urlparse.urlparse(url)
548 scheme = parsed_url[0]
549 if not scheme:
550 # A relative url. Fetch the real base.
551 path = parsed_url[2]
552 if path[0] != "/":
553 raise Error(
554 "relative DEPS entry \"%s\" must begin with a slash" % d)
555 # Create a scm just to query the full url.
msb@chromium.orgcb5442b2009-09-22 16:51:24 +0000556 scm = gclient_scm.CreateSCM(solution["url"], self._root_dir,
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000557 None)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000558 url = scm.FullUrlForRelativeUrl(url)
559 if d in deps and deps[d] != url:
560 raise Error(
561 "Solutions have conflicting versions of dependency \"%s\"" % d)
562 if d in solution_urls and solution_urls[d] != url:
563 raise Error(
564 "Dependency \"%s\" conflicts with specified solution" % d)
565 # Grab the dependency.
566 deps[d] = url
567 return deps
568
phajdan.jr@chromium.org71b40682009-07-31 23:40:09 +0000569 def _RunHookAction(self, hook_dict, matching_file_list):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000570 """Runs the action from a single hook.
571 """
572 command = hook_dict['action'][:]
573 if command[0] == 'python':
574 # If the hook specified "python" as the first item, the action is a
575 # Python script. Run it by starting a new copy of the same
576 # interpreter.
577 command[0] = sys.executable
578
phajdan.jr@chromium.org71b40682009-07-31 23:40:09 +0000579 if '$matching_files' in command:
phajdan.jr@chromium.org68f2e092009-08-06 17:05:35 +0000580 splice_index = command.index('$matching_files')
581 command[splice_index:splice_index + 1] = matching_file_list
phajdan.jr@chromium.org71b40682009-07-31 23:40:09 +0000582
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000583 # Use a discrete exit status code of 2 to indicate that a hook action
584 # failed. Users of this script may wish to treat hook action failures
585 # differently from VC failures.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000586 gclient_utils.SubprocessCall(command, self._root_dir, fail_status=2)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000587
588 def _RunHooks(self, command, file_list, is_using_git):
589 """Evaluates all hooks, running actions as needed.
590 """
591 # Hooks only run for these command types.
592 if not command in ('update', 'revert', 'runhooks'):
593 return
594
evan@chromium.org67820ef2009-07-27 17:23:00 +0000595 # Hooks only run when --nohooks is not specified
596 if self._options.nohooks:
597 return
598
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000599 # Get any hooks from the .gclient file.
600 hooks = self.GetVar("hooks", [])
601 # Add any hooks found in DEPS files.
602 hooks.extend(self._deps_hooks)
603
604 # If "--force" was specified, run all hooks regardless of what files have
605 # changed. If the user is using git, then we don't know what files have
606 # changed so we always run all hooks.
607 if self._options.force or is_using_git:
608 for hook_dict in hooks:
phajdan.jr@chromium.org71b40682009-07-31 23:40:09 +0000609 self._RunHookAction(hook_dict, [])
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000610 return
611
612 # Run hooks on the basis of whether the files from the gclient operation
613 # match each hook's pattern.
614 for hook_dict in hooks:
615 pattern = re.compile(hook_dict['pattern'])
phajdan.jr@chromium.org71b40682009-07-31 23:40:09 +0000616 matching_file_list = [file for file in file_list if pattern.search(file)]
617 if matching_file_list:
618 self._RunHookAction(hook_dict, matching_file_list)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000619
620 def RunOnDeps(self, command, args):
621 """Runs a command on each dependency in a client and its dependencies.
622
623 The module's dependencies are specified in its top-level DEPS files.
624
625 Args:
626 command: The command to use (e.g., 'status' or 'diff')
627 args: list of str - extra arguments to add to the command line.
628
629 Raises:
630 Error: If the client has conflicting entries.
631 """
632 if not command in self.supported_commands:
633 raise Error("'%s' is an unsupported command" % command)
634
635 # Check for revision overrides.
636 revision_overrides = {}
637 for revision in self._options.revisions:
638 if revision.find("@") == -1:
639 raise Error(
640 "Specify the full dependency when specifying a revision number.")
641 revision_elem = revision.split("@")
642 # Disallow conflicting revs
643 if revision_overrides.has_key(revision_elem[0]) and \
644 revision_overrides[revision_elem[0]] != revision_elem[1]:
645 raise Error(
646 "Conflicting revision numbers specified.")
647 revision_overrides[revision_elem[0]] = revision_elem[1]
648
649 solutions = self.GetVar("solutions")
650 if not solutions:
651 raise Error("No solution specified")
652
653 # When running runhooks --force, there's no need to consult the SCM.
654 # All known hooks are expected to run unconditionally regardless of working
655 # copy state, so skip the SCM status check.
656 run_scm = not (command == 'runhooks' and self._options.force)
657
658 entries = {}
659 entries_deps_content = {}
660 file_list = []
661 # Run on the base solutions first.
662 for solution in solutions:
663 name = solution["name"]
gspencer@google.comdf2d5902009-09-11 22:16:21 +0000664 deps_file = solution.get("deps_file", self._options.deps_file)
665 if '/' in deps_file or '\\' in deps_file:
666 raise Error("deps_file name must not be a path, just a filename.")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000667 if name in entries:
668 raise Error("solution %s specified more than once" % name)
669 url = solution["url"]
670 entries[name] = url
yaar@chromium.orgf1328042009-09-22 23:14:23 +0000671 if run_scm and url:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000672 self._options.revision = revision_overrides.get(name)
msb@chromium.orgcb5442b2009-09-22 16:51:24 +0000673 scm = gclient_scm.CreateSCM(url, self._root_dir, name)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000674 scm.RunCommand(command, self._options, args, file_list)
phajdan.jr@chromium.orgd83b2b22009-08-11 15:30:55 +0000675 file_list = [os.path.join(name, file.strip()) for file in file_list]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000676 self._options.revision = None
677 try:
678 deps_content = FileRead(os.path.join(self._root_dir, name,
gspencer@google.comdf2d5902009-09-11 22:16:21 +0000679 deps_file))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000680 except IOError, e:
681 if e.errno != errno.ENOENT:
682 raise
683 deps_content = ""
684 entries_deps_content[name] = deps_content
685
686 # Process the dependencies next (sort alphanumerically to ensure that
687 # containing directories get populated first and for readability)
688 deps = self._ParseAllDeps(entries, entries_deps_content)
689 deps_to_process = deps.keys()
690 deps_to_process.sort()
691
692 # First pass for direct dependencies.
693 for d in deps_to_process:
694 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
702
703 # Second pass for inherited deps (via the From keyword)
704 for d in deps_to_process:
705 if type(deps[d]) != str:
706 sub_deps = self._ParseSolutionDeps(
707 deps[d].module_name,
708 FileRead(os.path.join(self._root_dir,
709 deps[d].module_name,
710 self._options.deps_file)),
711 {})
712 url = sub_deps[d]
713 entries[d] = url
714 if run_scm:
715 self._options.revision = revision_overrides.get(d)
msb@chromium.orgcb5442b2009-09-22 16:51:24 +0000716 scm = gclient_scm.CreateSCM(url, self._root_dir, d)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000717 scm.RunCommand(command, self._options, args, file_list)
718 self._options.revision = None
gspencer@google.comdf2d5902009-09-11 22:16:21 +0000719
phajdan.jr@chromium.orgd83b2b22009-08-11 15:30:55 +0000720 # Convert all absolute paths to relative.
721 for i in range(len(file_list)):
722 # TODO(phajdan.jr): We should know exactly when the paths are absolute.
723 # It depends on the command being executed (like runhooks vs sync).
724 if not os.path.isabs(file_list[i]):
725 continue
726
727 prefix = os.path.commonprefix([self._root_dir.lower(),
728 file_list[i].lower()])
729 file_list[i] = file_list[i][len(prefix):]
730
731 # Strip any leading path separators.
732 while file_list[i].startswith('\\') or file_list[i].startswith('/'):
733 file_list[i] = file_list[i][1:]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000734
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000735 is_using_git = gclient_utils.IsUsingGit(self._root_dir, entries.keys())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000736 self._RunHooks(command, file_list, is_using_git)
737
738 if command == 'update':
ajwong@chromium.orgcdcee802009-06-23 15:30:42 +0000739 # Notify the user if there is an orphaned entry in their working copy.
740 # Only delete the directory if there are no changes in it, and
741 # delete_unversioned_trees is set to true.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000742 prev_entries = self._ReadEntries()
743 for entry in prev_entries:
maruel@chromium.orgc5e9aec2009-08-03 18:25:56 +0000744 # Fix path separator on Windows.
745 entry_fixed = entry.replace('/', os.path.sep)
746 e_dir = os.path.join(self._root_dir, entry_fixed)
747 # Use entry and not entry_fixed there.
maruel@chromium.org0329e672009-05-13 18:41:04 +0000748 if entry not in entries and os.path.exists(e_dir):
msb@chromium.org2e38de72009-09-28 17:04:47 +0000749 file_list = []
750 scm = gclient_scm.CreateSCM(prev_entries[entry], self._root_dir,
751 entry_fixed)
752 scm.status(self._options, [], file_list)
753 if not self._options.delete_unversioned_trees or file_list:
maruel@chromium.orgc5e9aec2009-08-03 18:25:56 +0000754 # There are modified files in this entry. Keep warning until
755 # removed.
maruel@chromium.orgc5e9aec2009-08-03 18:25:56 +0000756 print(("\nWARNING: \"%s\" is no longer part of this client. "
757 "It is recommended that you manually remove it.\n") %
758 entry_fixed)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000759 else:
760 # Delete the entry
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000761 print("\n________ deleting \'%s\' " +
maruel@chromium.orgc5e9aec2009-08-03 18:25:56 +0000762 "in \'%s\'") % (entry_fixed, self._root_dir)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000763 gclient_utils.RemoveDirectory(e_dir)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000764 # record the current list of entries for next time
765 self._SaveEntries(entries)
766
767 def PrintRevInfo(self):
768 """Output revision info mapping for the client and its dependencies. This
769 allows the capture of a overall "revision" for the source tree that can
770 be used to reproduce the same tree in the future. The actual output
771 contains enough information (source paths, svn server urls and revisions)
772 that it can be used either to generate external svn commands (without
773 gclient) or as input to gclient's --rev option (with some massaging of
774 the data).
775
776 NOTE: Unlike RunOnDeps this does not require a local checkout and is run
777 on the Pulse master. It MUST NOT execute hooks.
778
779 Raises:
780 Error: If the client has conflicting entries.
781 """
782 # Check for revision overrides.
783 revision_overrides = {}
784 for revision in self._options.revisions:
785 if revision.find("@") < 0:
786 raise Error(
787 "Specify the full dependency when specifying a revision number.")
788 revision_elem = revision.split("@")
789 # Disallow conflicting revs
790 if revision_overrides.has_key(revision_elem[0]) and \
791 revision_overrides[revision_elem[0]] != revision_elem[1]:
792 raise Error(
793 "Conflicting revision numbers specified.")
794 revision_overrides[revision_elem[0]] = revision_elem[1]
795
796 solutions = self.GetVar("solutions")
797 if not solutions:
798 raise Error("No solution specified")
799
800 entries = {}
801 entries_deps_content = {}
802
803 # Inner helper to generate base url and rev tuple (including honoring
804 # |revision_overrides|)
805 def GetURLAndRev(name, original_url):
806 if original_url.find("@") < 0:
807 if revision_overrides.has_key(name):
msb@chromium.org770ff9e2009-09-23 17:18:18 +0000808 return (original_url, revision_overrides[name])
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000809 else:
810 # TODO(aharper): SVN/SCMWrapper cleanup (non-local commandset)
maruel@chromium.org167b9e62009-09-17 17:41:02 +0000811 return (original_url,
812 gclient_scm.CaptureSVNHeadRevision(original_url))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000813 else:
814 url_components = original_url.split("@")
815 if revision_overrides.has_key(name):
msb@chromium.org770ff9e2009-09-23 17:18:18 +0000816 return (url_components[0], revision_overrides[name])
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000817 else:
msb@chromium.org770ff9e2009-09-23 17:18:18 +0000818 return (url_components[0], url_components[1])
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000819
820 # Run on the base solutions first.
821 for solution in solutions:
822 name = solution["name"]
823 if name in entries:
824 raise Error("solution %s specified more than once" % name)
825 (url, rev) = GetURLAndRev(name, solution["url"])
msb@chromium.org770ff9e2009-09-23 17:18:18 +0000826 entries[name] = "%s@%s" % (url, rev)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000827 # TODO(aharper): SVN/SCMWrapper cleanup (non-local commandset)
maruel@chromium.org167b9e62009-09-17 17:41:02 +0000828 entries_deps_content[name] = gclient_scm.CaptureSVN(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000829 ["cat",
msb@chromium.org770ff9e2009-09-23 17:18:18 +0000830 "%s/%s@%s" % (url,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000831 self._options.deps_file,
832 rev)],
833 os.getcwd())
834
835 # Process the dependencies next (sort alphanumerically to ensure that
836 # containing directories get populated first and for readability)
837 deps = self._ParseAllDeps(entries, entries_deps_content)
838 deps_to_process = deps.keys()
839 deps_to_process.sort()
840
841 # First pass for direct dependencies.
842 for d in deps_to_process:
843 if type(deps[d]) == str:
844 (url, rev) = GetURLAndRev(d, deps[d])
msb@chromium.org770ff9e2009-09-23 17:18:18 +0000845 entries[d] = "%s@%s" % (url, rev)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000846
847 # Second pass for inherited deps (via the From keyword)
848 for d in deps_to_process:
849 if type(deps[d]) != str:
850 deps_parent_url = entries[deps[d].module_name]
851 if deps_parent_url.find("@") < 0:
852 raise Error("From %s missing revisioned url" % deps[d].module_name)
853 deps_parent_url_components = deps_parent_url.split("@")
854 # TODO(aharper): SVN/SCMWrapper cleanup (non-local commandset)
maruel@chromium.org167b9e62009-09-17 17:41:02 +0000855 deps_parent_content = gclient_scm.CaptureSVN(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000856 ["cat",
857 "%s/%s@%s" % (deps_parent_url_components[0],
858 self._options.deps_file,
859 deps_parent_url_components[1])],
860 os.getcwd())
861 sub_deps = self._ParseSolutionDeps(
862 deps[d].module_name,
863 FileRead(os.path.join(self._root_dir,
864 deps[d].module_name,
865 self._options.deps_file)),
866 {})
867 (url, rev) = GetURLAndRev(d, sub_deps[d])
msb@chromium.org770ff9e2009-09-23 17:18:18 +0000868 entries[d] = "%s@%s" % (url, rev)
maruel@chromium.org57e893e2009-08-19 18:12:09 +0000869 print(";\n\n".join(["%s: %s" % (x, entries[x])
870 for x in sorted(entries.keys())]))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000871
872
873## gclient commands.
874
875
876def DoCleanup(options, args):
877 """Handle the cleanup subcommand.
878
879 Raises:
880 Error: if client isn't configured properly.
881 """
maruel@chromium.org2806acc2009-05-15 12:33:34 +0000882 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000883 if not client:
884 raise Error("client not configured; see 'gclient config'")
885 if options.verbose:
886 # Print out the .gclient file. This is longer than if we just printed the
887 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000888 print(client.ConfigContent())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000889 options.verbose = True
890 return client.RunOnDeps('cleanup', args)
891
892
893def DoConfig(options, args):
894 """Handle the config subcommand.
895
896 Args:
897 options: If options.spec set, a string providing contents of config file.
898 args: The command line args. If spec is not set,
899 then args[0] is a string URL to get for config file.
900
901 Raises:
902 Error: on usage error
903 """
904 if len(args) < 1 and not options.spec:
905 raise Error("required argument missing; see 'gclient help config'")
maruel@chromium.org0329e672009-05-13 18:41:04 +0000906 if os.path.exists(options.config_filename):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000907 raise Error("%s file already exists in the current directory" %
908 options.config_filename)
maruel@chromium.org2806acc2009-05-15 12:33:34 +0000909 client = GClient('.', options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000910 if options.spec:
911 client.SetConfig(options.spec)
912 else:
913 # TODO(darin): it would be nice to be able to specify an alternate relpath
914 # for the given URL.
maruel@chromium.org1ab7ffc2009-06-03 17:21:37 +0000915 base_url = args[0].rstrip('/')
916 name = base_url.split("/")[-1]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000917 safesync_url = ""
918 if len(args) > 1:
919 safesync_url = args[1]
920 client.SetDefaultConfig(name, base_url, safesync_url)
921 client.SaveConfig()
922
923
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +0000924def DoExport(options, args):
925 """Handle the export subcommand.
phajdan.jr@chromium.org71b40682009-07-31 23:40:09 +0000926
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +0000927 Raises:
928 Error: on usage error
929 """
930 if len(args) != 1:
931 raise Error("Need directory name")
932 client = GClient.LoadCurrentConfig(options)
933
934 if not client:
935 raise Error("client not configured; see 'gclient config'")
936
937 if options.verbose:
938 # Print out the .gclient file. This is longer than if we just printed the
939 # client dict, but more legible, and it might contain helpful comments.
940 print(client.ConfigContent())
941 return client.RunOnDeps('export', args)
942
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000943def DoHelp(options, args):
944 """Handle the help subcommand giving help for another subcommand.
945
946 Raises:
947 Error: if the command is unknown.
948 """
949 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:
952 raise Error("unknown subcommand '%s'; see 'gclient help'" % args[0])
953
954
kbr@google.comab318592009-09-04 00:54:55 +0000955def DoPack(options, args):
956 """Handle the pack subcommand.
957
958 Raises:
959 Error: if client isn't configured properly.
960 """
961 client = GClient.LoadCurrentConfig(options)
962 if not client:
963 raise Error("client not configured; see 'gclient config'")
964 if options.verbose:
965 # Print out the .gclient file. This is longer than if we just printed the
966 # client dict, but more legible, and it might contain helpful comments.
967 print(client.ConfigContent())
968 options.verbose = True
969 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:
980 raise Error("client not configured; see 'gclient config'")
981 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 options.verbose = True
986 return client.RunOnDeps('status', args)
987
988
989def DoUpdate(options, args):
990 """Handle the update and sync subcommands.
991
992 Raises:
993 Error: if client isn't configured properly.
994 """
maruel@chromium.org2806acc2009-05-15 12:33:34 +0000995 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000996
997 if not client:
998 raise Error("client not configured; see 'gclient config'")
999
1000 if not options.head:
1001 solutions = client.GetVar('solutions')
1002 if solutions:
1003 for s in solutions:
1004 if s.get('safesync_url', ''):
1005 # rip through revisions and make sure we're not over-riding
1006 # something that was explicitly passed
1007 has_key = False
1008 for r in options.revisions:
1009 if r.split('@')[0] == s['name']:
1010 has_key = True
1011 break
1012
1013 if not has_key:
1014 handle = urllib.urlopen(s['safesync_url'])
1015 rev = handle.read().strip()
1016 handle.close()
1017 if len(rev):
1018 options.revisions.append(s['name']+'@'+rev)
1019
1020 if options.verbose:
1021 # Print out the .gclient file. This is longer than if we just printed the
1022 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +00001023 print(client.ConfigContent())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001024 return client.RunOnDeps('update', args)
1025
1026
1027def DoDiff(options, args):
1028 """Handle the diff subcommand.
1029
1030 Raises:
1031 Error: if client isn't configured properly.
1032 """
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001033 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001034 if not client:
1035 raise Error("client not configured; see 'gclient config'")
1036 if options.verbose:
1037 # Print out the .gclient file. This is longer than if we just printed the
1038 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +00001039 print(client.ConfigContent())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001040 options.verbose = True
1041 return client.RunOnDeps('diff', args)
1042
1043
1044def DoRevert(options, args):
1045 """Handle the revert subcommand.
1046
1047 Raises:
1048 Error: if client isn't configured properly.
1049 """
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001050 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001051 if not client:
1052 raise Error("client not configured; see 'gclient config'")
1053 return client.RunOnDeps('revert', args)
1054
1055
1056def DoRunHooks(options, args):
1057 """Handle the runhooks subcommand.
1058
1059 Raises:
1060 Error: if client isn't configured properly.
1061 """
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001062 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001063 if not client:
1064 raise Error("client not configured; see 'gclient config'")
1065 if options.verbose:
1066 # Print out the .gclient file. This is longer than if we just printed the
1067 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +00001068 print(client.ConfigContent())
maruel@chromium.org5df6a462009-08-28 18:52:26 +00001069 options.force = True
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001070 return client.RunOnDeps('runhooks', args)
1071
1072
1073def DoRevInfo(options, args):
1074 """Handle the revinfo subcommand.
1075
1076 Raises:
1077 Error: if client isn't configured properly.
1078 """
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001079 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001080 if not client:
1081 raise Error("client not configured; see 'gclient config'")
1082 client.PrintRevInfo()
1083
1084
1085gclient_command_map = {
1086 "cleanup": DoCleanup,
1087 "config": DoConfig,
1088 "diff": DoDiff,
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +00001089 "export": DoExport,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001090 "help": DoHelp,
kbr@google.comab318592009-09-04 00:54:55 +00001091 "pack": DoPack,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001092 "status": DoStatus,
1093 "sync": DoUpdate,
1094 "update": DoUpdate,
1095 "revert": DoRevert,
1096 "runhooks": DoRunHooks,
1097 "revinfo" : DoRevInfo,
1098}
1099
1100
1101def DispatchCommand(command, options, args, command_map=None):
1102 """Dispatches the appropriate subcommand based on command line arguments."""
1103 if command_map is None:
1104 command_map = gclient_command_map
1105
1106 if command in command_map:
1107 return command_map[command](options, args)
1108 else:
1109 raise Error("unknown subcommand '%s'; see 'gclient help'" % command)
1110
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"))
1135 option_parser.add_option("", "--spec", default=None,
1136 help=("(config only) create a gclient file "
1137 "containing the provided string"))
1138 option_parser.add_option("", "--verbose", action="store_true", default=False,
1139 help="produce additional output for diagnostics")
1140 option_parser.add_option("", "--manually_grab_svn_rev", action="store_true",
1141 default=False,
1142 help="Skip svn up whenever possible by requesting "
1143 "actual HEAD revision from the repository")
1144 option_parser.add_option("", "--head", action="store_true", default=False,
1145 help=("skips any safesync_urls specified in "
1146 "configured solutions"))
ajwong@chromium.orgcdcee802009-06-23 15:30:42 +00001147 option_parser.add_option("", "--delete_unversioned_trees",
1148 action="store_true", default=False,
1149 help=("on update, delete any unexpected "
1150 "unversioned trees that are in the checkout"))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001151
1152 if len(argv) < 2:
1153 # Users don't need to be told to use the 'help' command.
1154 option_parser.print_help()
1155 return 1
1156 # Add manual support for --version as first argument.
1157 if argv[1] == '--version':
1158 option_parser.print_version()
1159 return 0
1160
1161 # Add manual support for --help as first argument.
1162 if argv[1] == '--help':
1163 argv[1] = 'help'
1164
1165 command = argv[1]
1166 options, args = option_parser.parse_args(argv[2:])
1167
1168 if len(argv) < 3 and command == "help":
1169 option_parser.print_help()
1170 return 0
1171
maruel@chromium.org754960e2009-09-21 12:31:05 +00001172 if options.verbose:
1173 logging.basicConfig(level=logging.DEBUG)
1174
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001175 # Files used for configuration and state saving.
1176 options.config_filename = os.environ.get("GCLIENT_FILE", ".gclient")
1177 options.entries_filename = ".gclient_entries"
1178 options.deps_file = "DEPS"
1179
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001180 options.platform = sys.platform
1181 return DispatchCommand(command, options, args)
1182
1183
1184if "__main__" == __name__:
1185 try:
1186 result = Main(sys.argv)
1187 except Error, e:
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +00001188 print >> sys.stderr, "Error: %s" % str(e)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001189 result = 1
1190 sys.exit(result)
1191
1192# vim: ts=2:sw=2:tw=80:et: