blob: fbc16cc6d12db23faa87a3548596803e7ea77406 [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
maruel@chromium.org79692d62010-05-14 18:57:13 +000058__version__ = "0.3.5"
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000059
60import errno
maruel@chromium.org754960e2009-09-21 12:31:05 +000061import logging
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000062import optparse
63import os
msb@chromium.org2e38de72009-09-28 17:04:47 +000064import pprint
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000065import re
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000066import sys
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000067import urlparse
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000068import urllib
69
maruel@chromium.orgada4c652009-12-03 15:32:01 +000070import breakpad
71
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000072import gclient_scm
73import gclient_utils
nasser@codeaurora.org1f7a3d12010-02-04 15:11:50 +000074from third_party.repo.progress import Progress
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000075
76# default help text
77DEFAULT_USAGE_TEXT = (
msb@chromium.orgd6504212010-01-13 17:34:31 +000078"""usage: %prog <subcommand> [options] [--] [SCM options/args...]
79a wrapper for managing a set of svn client modules and/or git repositories.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000080Version """ + __version__ + """
81
82subcommands:
83 cleanup
84 config
85 diff
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +000086 export
kbr@google.comab318592009-09-04 00:54:55 +000087 pack
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000088 revert
89 status
90 sync
91 update
92 runhooks
93 revinfo
94
msb@chromium.orgd6504212010-01-13 17:34:31 +000095Options and extra arguments can be passed to invoked SCM commands by
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000096appending them to the command line. Note that if the first such
97appended option starts with a dash (-) then the options must be
98preceded by -- to distinguish them from gclient options.
99
100For additional help on a subcommand or examples of usage, try
101 %prog help <subcommand>
102 %prog help files
103""")
104
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000105
maruel@chromium.org1f7d1182010-05-17 18:17:38 +0000106def attr(attr, data):
107 """Sets an attribute on a function."""
108 def hook(fn):
109 setattr(fn, attr, data)
110 return fn
111 return hook
maruel@chromium.orge3da35f2010-03-09 21:40:45 +0000112
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000113
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000114## GClient implementation.
115
116
117class GClient(object):
118 """Object that represent a gclient checkout."""
119
120 supported_commands = [
kbr@google.comab318592009-09-04 00:54:55 +0000121 'cleanup', 'diff', 'export', 'pack', 'revert', 'status', 'update',
122 'runhooks'
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000123 ]
124
maruel@chromium.org1f7d1182010-05-17 18:17:38 +0000125 DEPS_FILE = 'DEPS'
126
127 DEFAULT_CLIENT_FILE_TEXT = ("""\
128solutions = [
129 { "name" : "%(solution_name)s",
130 "url" : "%(solution_url)s",
131 "custom_deps" : {
132 },
133 "safesync_url": "%(safesync_url)s"
134 },
135]
136""")
137
138 DEFAULT_SNAPSHOT_SOLUTION_TEXT = ("""\
139 { "name" : "%(solution_name)s",
140 "url" : "%(solution_url)s",
141 "custom_deps" : {
142 %(solution_deps)s,
143 },
144 "safesync_url": "%(safesync_url)s"
145 },
146""")
147
148 DEFAULT_SNAPSHOT_FILE_TEXT = ("""\
149# Snapshot generated with gclient revinfo --snapshot
150solutions = [
151%(solution_list)s
152]
153""")
154
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000155 def __init__(self, root_dir, options):
156 self._root_dir = root_dir
157 self._options = options
158 self._config_content = None
159 self._config_dict = {}
160 self._deps_hooks = []
161
162 def SetConfig(self, content):
163 self._config_dict = {}
164 self._config_content = content
skylined@chromium.orgdf0032c2009-05-29 10:43:56 +0000165 try:
166 exec(content, self._config_dict)
167 except SyntaxError, e:
168 try:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000169 __pychecker__ = 'no-objattrs'
skylined@chromium.orgdf0032c2009-05-29 10:43:56 +0000170 # Try to construct a human readable error message
171 error_message = [
172 'There is a syntax error in your configuration file.',
173 'Line #%s, character %s:' % (e.lineno, e.offset),
174 '"%s"' % re.sub(r'[\r\n]*$', '', e.text) ]
175 except:
176 # Something went wrong, re-raise the original exception
177 raise e
178 else:
179 # Raise a new exception with the human readable message:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000180 raise gclient_utils.Error('\n'.join(error_message))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000181
182 def SaveConfig(self):
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000183 gclient_utils.FileWrite(os.path.join(self._root_dir,
184 self._options.config_filename),
185 self._config_content)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000186
187 def _LoadConfig(self):
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000188 client_source = gclient_utils.FileRead(
189 os.path.join(self._root_dir, self._options.config_filename))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000190 self.SetConfig(client_source)
191
192 def ConfigContent(self):
193 return self._config_content
194
195 def GetVar(self, key, default=None):
196 return self._config_dict.get(key, default)
197
198 @staticmethod
199 def LoadCurrentConfig(options, from_dir=None):
200 """Searches for and loads a .gclient file relative to the current working
201 dir.
202
203 Returns:
204 A dict representing the contents of the .gclient file or an empty dict if
205 the .gclient file doesn't exist.
206 """
207 if not from_dir:
208 from_dir = os.curdir
209 path = os.path.realpath(from_dir)
maruel@chromium.org0329e672009-05-13 18:41:04 +0000210 while not os.path.exists(os.path.join(path, options.config_filename)):
maruel@chromium.org55e724e2010-03-11 19:36:49 +0000211 split_path = os.path.split(path)
212 if not split_path[1]:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000213 return None
maruel@chromium.org55e724e2010-03-11 19:36:49 +0000214 path = split_path[0]
maruel@chromium.org2806acc2009-05-15 12:33:34 +0000215 client = GClient(path, options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000216 client._LoadConfig()
217 return client
218
219 def SetDefaultConfig(self, solution_name, solution_url, safesync_url):
maruel@chromium.org1f7d1182010-05-17 18:17:38 +0000220 self.SetConfig(self.DEFAULT_CLIENT_FILE_TEXT % {
gspencer@google.comdf2d5902009-09-11 22:16:21 +0000221 'solution_name': solution_name,
222 'solution_url': solution_url,
223 'safesync_url' : safesync_url,
224 })
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000225
226 def _SaveEntries(self, entries):
227 """Creates a .gclient_entries file to record the list of unique checkouts.
228
229 The .gclient_entries file lives in the same directory as .gclient.
230
231 Args:
232 entries: A sequence of solution names.
233 """
maruel@chromium.orge41f6822010-04-08 16:37:06 +0000234 # Sometimes pprint.pformat will use {', sometimes it'll use { ' ... It
235 # makes testing a bit too fun.
236 result = pprint.pformat(entries, 2)
237 if result.startswith('{\''):
maruel@chromium.org1edec4d2010-04-08 17:17:06 +0000238 result = '{ \'' + result[2:]
maruel@chromium.orge41f6822010-04-08 16:37:06 +0000239 text = "entries = \\\n" + result + '\n'
msb@chromium.org2e38de72009-09-28 17:04:47 +0000240 file_path = os.path.join(self._root_dir, self._options.entries_filename)
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000241 gclient_utils.FileWrite(file_path, text)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000242
243 def _ReadEntries(self):
244 """Read the .gclient_entries file for the given client.
245
246 Args:
247 client: The client for which the entries file should be read.
248
249 Returns:
250 A sequence of solution names, which will be empty if there is the
251 entries file hasn't been created yet.
252 """
253 scope = {}
254 filename = os.path.join(self._root_dir, self._options.entries_filename)
maruel@chromium.org0329e672009-05-13 18:41:04 +0000255 if not os.path.exists(filename):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000256 return []
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000257 exec(gclient_utils.FileRead(filename), scope)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000258 return scope["entries"]
259
260 class FromImpl:
261 """Used to implement the From syntax."""
262
tony@chromium.org30ef9ae2010-04-09 02:18:05 +0000263 def __init__(self, module_name, sub_target_name=None):
264 """module_name is the dep module we want to include from. It can also be
265 the name of a subdirectory to include from.
266
267 sub_target_name is an optional parameter if the module name in the other
268 DEPS file is different. E.g., you might want to map src/net to net."""
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000269 self.module_name = module_name
tony@chromium.org30ef9ae2010-04-09 02:18:05 +0000270 self.sub_target_name = sub_target_name
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000271
272 def __str__(self):
tony@chromium.org30ef9ae2010-04-09 02:18:05 +0000273 return 'From(%s, %s)' % (repr(self.module_name),
274 repr(self.sub_target_name))
275
276 def GetUrl(self, target_name, sub_deps_base_url, root_dir, sub_deps):
277 """Resolve the URL for this From entry."""
278 sub_deps_target_name = target_name
279 if self.sub_target_name:
280 sub_deps_target_name = self.sub_target_name
281 url = sub_deps[sub_deps_target_name]
282 if url.startswith('/'):
283 # If it's a relative URL, we need to resolve the URL relative to the
284 # sub deps base URL.
285 if not isinstance(sub_deps_base_url, basestring):
286 sub_deps_base_url = sub_deps_base_url.GetPath()
287 scm = gclient_scm.CreateSCM(sub_deps_base_url, root_dir,
288 None)
289 url = scm.FullUrlForRelativeUrl(url)
290 return url
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000291
tony@chromium.org4b5b1772010-04-08 01:52:56 +0000292 class FileImpl:
293 """Used to implement the File('') syntax which lets you sync a single file
294 from an SVN repo."""
295
296 def __init__(self, file_location):
297 self.file_location = file_location
298
299 def __str__(self):
300 return 'File("%s")' % self.file_location
301
302 def GetPath(self):
303 return os.path.split(self.file_location)[0]
304
305 def GetFilename(self):
306 rev_tokens = self.file_location.split('@')
307 return os.path.split(rev_tokens[0])[1]
308
309 def GetRevision(self):
310 rev_tokens = self.file_location.split('@')
311 if len(rev_tokens) > 1:
312 return rev_tokens[1]
313 return None
314
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000315 class _VarImpl:
316 def __init__(self, custom_vars, local_scope):
317 self._custom_vars = custom_vars
318 self._local_scope = local_scope
319
320 def Lookup(self, var_name):
321 """Implements the Var syntax."""
322 if var_name in self._custom_vars:
323 return self._custom_vars[var_name]
324 elif var_name in self._local_scope.get("vars", {}):
325 return self._local_scope["vars"][var_name]
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000326 raise gclient_utils.Error("Var is not defined: %s" % var_name)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000327
328 def _ParseSolutionDeps(self, solution_name, solution_deps_content,
tony@chromium.org30ef9ae2010-04-09 02:18:05 +0000329 custom_vars, parse_hooks):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000330 """Parses the DEPS file for the specified solution.
331
332 Args:
333 solution_name: The name of the solution to query.
334 solution_deps_content: Content of the DEPS file for the solution
335 custom_vars: A dict of vars to override any vars defined in the DEPS file.
336
337 Returns:
338 A dict mapping module names (as relative paths) to URLs or an empty
339 dict if the solution does not have a DEPS file.
340 """
341 # Skip empty
342 if not solution_deps_content:
343 return {}
344 # Eval the content
345 local_scope = {}
346 var = self._VarImpl(custom_vars, local_scope)
tony@chromium.org4b5b1772010-04-08 01:52:56 +0000347 global_scope = {
348 "File": self.FileImpl,
349 "From": self.FromImpl,
350 "Var": var.Lookup,
351 "deps_os": {},
352 }
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000353 exec(solution_deps_content, global_scope, local_scope)
354 deps = local_scope.get("deps", {})
355
356 # load os specific dependencies if defined. these dependencies may
357 # override or extend the values defined by the 'deps' member.
358 if "deps_os" in local_scope:
359 deps_os_choices = {
360 "win32": "win",
361 "win": "win",
362 "cygwin": "win",
363 "darwin": "mac",
364 "mac": "mac",
365 "unix": "unix",
366 "linux": "unix",
367 "linux2": "unix",
368 }
369
370 if self._options.deps_os is not None:
371 deps_to_include = self._options.deps_os.split(",")
372 if "all" in deps_to_include:
maruel@chromium.org55e724e2010-03-11 19:36:49 +0000373 deps_to_include = list(set(deps_os_choices.itervalues()))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000374 else:
maruel@chromium.org1f7d1182010-05-17 18:17:38 +0000375 deps_to_include = [deps_os_choices.get(sys.platform, "unix")]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000376
377 deps_to_include = set(deps_to_include)
378 for deps_os_key in deps_to_include:
379 os_deps = local_scope["deps_os"].get(deps_os_key, {})
380 if len(deps_to_include) > 1:
381 # Ignore any overrides when including deps for more than one
382 # platform, so we collect the broadest set of dependencies available.
383 # We may end up with the wrong revision of something for our
384 # platform, but this is the best we can do.
385 deps.update([x for x in os_deps.items() if not x[0] in deps])
386 else:
387 deps.update(os_deps)
388
tony@chromium.org30ef9ae2010-04-09 02:18:05 +0000389 if 'hooks' in local_scope and parse_hooks:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000390 self._deps_hooks.extend(local_scope['hooks'])
391
392 # If use_relative_paths is set in the DEPS file, regenerate
393 # the dictionary using paths relative to the directory containing
394 # the DEPS file.
395 if local_scope.get('use_relative_paths'):
396 rel_deps = {}
397 for d, url in deps.items():
398 # normpath is required to allow DEPS to use .. in their
399 # dependency local path.
400 rel_deps[os.path.normpath(os.path.join(solution_name, d))] = url
401 return rel_deps
402 else:
403 return deps
404
405 def _ParseAllDeps(self, solution_urls, solution_deps_content):
406 """Parse the complete list of dependencies for the client.
407
408 Args:
409 solution_urls: A dict mapping module names (as relative paths) to URLs
410 corresponding to the solutions specified by the client. This parameter
411 is passed as an optimization.
412 solution_deps_content: A dict mapping module names to the content
413 of their DEPS files
414
415 Returns:
416 A dict mapping module names (as relative paths) to URLs corresponding
417 to the entire set of dependencies to checkout for the given client.
418
419 Raises:
420 Error: If a dependency conflicts with another dependency or of a solution.
421 """
422 deps = {}
423 for solution in self.GetVar("solutions"):
424 custom_vars = solution.get("custom_vars", {})
425 solution_deps = self._ParseSolutionDeps(
426 solution["name"],
427 solution_deps_content[solution["name"]],
tony@chromium.org30ef9ae2010-04-09 02:18:05 +0000428 custom_vars,
429 True)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000430
431 # If a line is in custom_deps, but not in the solution, we want to append
432 # this line to the solution.
433 if "custom_deps" in solution:
434 for d in solution["custom_deps"]:
435 if d not in solution_deps:
436 solution_deps[d] = solution["custom_deps"][d]
437
438 for d in solution_deps:
439 if "custom_deps" in solution and d in solution["custom_deps"]:
440 # Dependency is overriden.
441 url = solution["custom_deps"][d]
442 if url is None:
443 continue
444 else:
445 url = solution_deps[d]
446 # if we have a From reference dependent on another solution, then
447 # just skip the From reference. When we pull deps for the solution,
448 # we will take care of this dependency.
449 #
450 # If multiple solutions all have the same From reference, then we
451 # should only add one to our list of dependencies.
tony@chromium.org4b5b1772010-04-08 01:52:56 +0000452 if isinstance(url, self.FromImpl):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000453 if url.module_name in solution_urls:
454 # Already parsed.
455 continue
456 if d in deps and type(deps[d]) != str:
457 if url.module_name == deps[d].module_name:
458 continue
tony@chromium.org4b5b1772010-04-08 01:52:56 +0000459 elif isinstance(url, str):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000460 parsed_url = urlparse.urlparse(url)
461 scheme = parsed_url[0]
462 if not scheme:
463 # A relative url. Fetch the real base.
464 path = parsed_url[2]
465 if path[0] != "/":
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000466 raise gclient_utils.Error(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000467 "relative DEPS entry \"%s\" must begin with a slash" % d)
msb@chromium.orge6f78352010-01-13 17:05:33 +0000468 # Create a scm just to query the full url.
469 scm = gclient_scm.CreateSCM(solution["url"], self._root_dir,
470 None)
471 url = scm.FullUrlForRelativeUrl(url)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000472 if d in deps and deps[d] != url:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000473 raise gclient_utils.Error(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000474 "Solutions have conflicting versions of dependency \"%s\"" % d)
475 if d in solution_urls and solution_urls[d] != url:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000476 raise gclient_utils.Error(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000477 "Dependency \"%s\" conflicts with specified solution" % d)
478 # Grab the dependency.
479 deps[d] = url
480 return deps
481
phajdan.jr@chromium.org71b40682009-07-31 23:40:09 +0000482 def _RunHookAction(self, hook_dict, matching_file_list):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000483 """Runs the action from a single hook.
484 """
485 command = hook_dict['action'][:]
486 if command[0] == 'python':
487 # If the hook specified "python" as the first item, the action is a
488 # Python script. Run it by starting a new copy of the same
489 # interpreter.
490 command[0] = sys.executable
491
phajdan.jr@chromium.org71b40682009-07-31 23:40:09 +0000492 if '$matching_files' in command:
phajdan.jr@chromium.org68f2e092009-08-06 17:05:35 +0000493 splice_index = command.index('$matching_files')
494 command[splice_index:splice_index + 1] = matching_file_list
phajdan.jr@chromium.org71b40682009-07-31 23:40:09 +0000495
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000496 # Use a discrete exit status code of 2 to indicate that a hook action
497 # failed. Users of this script may wish to treat hook action failures
498 # differently from VC failures.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000499 gclient_utils.SubprocessCall(command, self._root_dir, fail_status=2)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000500
501 def _RunHooks(self, command, file_list, is_using_git):
502 """Evaluates all hooks, running actions as needed.
503 """
504 # Hooks only run for these command types.
505 if not command in ('update', 'revert', 'runhooks'):
506 return
507
evan@chromium.org67820ef2009-07-27 17:23:00 +0000508 # Hooks only run when --nohooks is not specified
509 if self._options.nohooks:
510 return
511
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000512 # Get any hooks from the .gclient file.
513 hooks = self.GetVar("hooks", [])
514 # Add any hooks found in DEPS files.
515 hooks.extend(self._deps_hooks)
516
517 # If "--force" was specified, run all hooks regardless of what files have
518 # changed. If the user is using git, then we don't know what files have
519 # changed so we always run all hooks.
520 if self._options.force or is_using_git:
521 for hook_dict in hooks:
phajdan.jr@chromium.org71b40682009-07-31 23:40:09 +0000522 self._RunHookAction(hook_dict, [])
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000523 return
524
525 # Run hooks on the basis of whether the files from the gclient operation
526 # match each hook's pattern.
527 for hook_dict in hooks:
528 pattern = re.compile(hook_dict['pattern'])
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000529 matching_file_list = [f for f in file_list if pattern.search(f)]
phajdan.jr@chromium.org71b40682009-07-31 23:40:09 +0000530 if matching_file_list:
531 self._RunHookAction(hook_dict, matching_file_list)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000532
533 def RunOnDeps(self, command, args):
534 """Runs a command on each dependency in a client and its dependencies.
535
536 The module's dependencies are specified in its top-level DEPS files.
537
538 Args:
539 command: The command to use (e.g., 'status' or 'diff')
540 args: list of str - extra arguments to add to the command line.
541
542 Raises:
543 Error: If the client has conflicting entries.
544 """
545 if not command in self.supported_commands:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000546 raise gclient_utils.Error("'%s' is an unsupported command" % command)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000547
548 # Check for revision overrides.
549 revision_overrides = {}
550 for revision in self._options.revisions:
551 if revision.find("@") == -1:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000552 raise gclient_utils.Error(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000553 "Specify the full dependency when specifying a revision number.")
554 revision_elem = revision.split("@")
555 # Disallow conflicting revs
556 if revision_overrides.has_key(revision_elem[0]) and \
557 revision_overrides[revision_elem[0]] != revision_elem[1]:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000558 raise gclient_utils.Error(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000559 "Conflicting revision numbers specified.")
560 revision_overrides[revision_elem[0]] = revision_elem[1]
561
562 solutions = self.GetVar("solutions")
563 if not solutions:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000564 raise gclient_utils.Error("No solution specified")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000565
566 # When running runhooks --force, there's no need to consult the SCM.
567 # All known hooks are expected to run unconditionally regardless of working
568 # copy state, so skip the SCM status check.
569 run_scm = not (command == 'runhooks' and self._options.force)
570
571 entries = {}
572 entries_deps_content = {}
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000573 file_list = []
574 # Run on the base solutions first.
575 for solution in solutions:
576 name = solution["name"]
maruel@chromium.org1f7d1182010-05-17 18:17:38 +0000577 deps_file = solution.get("deps_file", self.DEPS_FILE)
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000578 if '/' in deps_file or '\\' in deps_file:
579 raise gclient_utils.Error('deps_file name must not be a path, just a '
580 'filename.')
581 if name in entries:
582 raise gclient_utils.Error("solution %s specified more than once" % name)
583 url = solution["url"]
584 entries[name] = url
585 if run_scm and url:
586 self._options.revision = revision_overrides.get(name)
587 scm = gclient_scm.CreateSCM(url, self._root_dir, name)
588 scm.RunCommand(command, self._options, args, file_list)
589 file_list = [os.path.join(name, f.strip()) for f in file_list]
590 self._options.revision = None
591 try:
592 deps_content = gclient_utils.FileRead(
593 os.path.join(self._root_dir, name, deps_file))
594 except IOError, e:
595 if e.errno != errno.ENOENT:
596 raise
597 deps_content = ""
598 entries_deps_content[name] = deps_content
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000599
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000600 # Process the dependencies next (sort alphanumerically to ensure that
601 # containing directories get populated first and for readability)
602 deps = self._ParseAllDeps(entries, entries_deps_content)
603 deps_to_process = deps.keys()
604 deps_to_process.sort()
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000605
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000606 # First pass for direct dependencies.
607 if command == 'update' and not self._options.verbose:
608 pm = Progress('Syncing projects', len(deps_to_process))
609 for d in deps_to_process:
nasser@codeaurora.org1f7a3d12010-02-04 15:11:50 +0000610 if command == 'update' and not self._options.verbose:
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000611 pm.update()
612 if type(deps[d]) == str:
613 url = deps[d]
614 entries[d] = url
615 if run_scm:
616 self._options.revision = revision_overrides.get(d)
617 scm = gclient_scm.CreateSCM(url, self._root_dir, d)
618 scm.RunCommand(command, self._options, args, file_list)
619 self._options.revision = None
620 elif isinstance(deps[d], self.FileImpl):
621 file = deps[d]
622 self._options.revision = file.GetRevision()
623 if run_scm:
624 scm = gclient_scm.CreateSCM(file.GetPath(), self._root_dir, d)
625 scm.RunCommand("updatesingle", self._options,
626 args + [file.GetFilename()], file_list)
maruel@chromium.org79692d62010-05-14 18:57:13 +0000627
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000628 if command == 'update' and not self._options.verbose:
629 pm.end()
piman@chromium.org6f363722010-04-27 00:41:09 +0000630
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000631 # Second pass for inherited deps (via the From keyword)
632 for d in deps_to_process:
633 if isinstance(deps[d], self.FromImpl):
634 filename = os.path.join(self._root_dir,
635 deps[d].module_name,
maruel@chromium.org1f7d1182010-05-17 18:17:38 +0000636 self.DEPS_FILE)
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000637 content = gclient_utils.FileRead(filename)
638 sub_deps = self._ParseSolutionDeps(deps[d].module_name, content, {},
639 False)
640 # Getting the URL from the sub_deps file can involve having to resolve
641 # a File() or having to resolve a relative URL. To resolve relative
642 # URLs, we need to pass in the orignal sub deps URL.
643 sub_deps_base_url = deps[deps[d].module_name]
644 url = deps[d].GetUrl(d, sub_deps_base_url, self._root_dir, sub_deps)
645 entries[d] = url
646 if run_scm:
647 self._options.revision = revision_overrides.get(d)
648 scm = gclient_scm.CreateSCM(url, self._root_dir, d)
649 scm.RunCommand(command, self._options, args, file_list)
650 self._options.revision = None
gspencer@google.comdf2d5902009-09-11 22:16:21 +0000651
phajdan.jr@chromium.orgd83b2b22009-08-11 15:30:55 +0000652 # Convert all absolute paths to relative.
653 for i in range(len(file_list)):
654 # TODO(phajdan.jr): We should know exactly when the paths are absolute.
655 # It depends on the command being executed (like runhooks vs sync).
656 if not os.path.isabs(file_list[i]):
657 continue
658
659 prefix = os.path.commonprefix([self._root_dir.lower(),
660 file_list[i].lower()])
661 file_list[i] = file_list[i][len(prefix):]
662
663 # Strip any leading path separators.
664 while file_list[i].startswith('\\') or file_list[i].startswith('/'):
665 file_list[i] = file_list[i][1:]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000666
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000667 is_using_git = gclient_utils.IsUsingGit(self._root_dir, entries.keys())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000668 self._RunHooks(command, file_list, is_using_git)
669
670 if command == 'update':
ajwong@chromium.orgcdcee802009-06-23 15:30:42 +0000671 # Notify the user if there is an orphaned entry in their working copy.
672 # Only delete the directory if there are no changes in it, and
673 # delete_unversioned_trees is set to true.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000674 prev_entries = self._ReadEntries()
675 for entry in prev_entries:
maruel@chromium.orgc5e9aec2009-08-03 18:25:56 +0000676 # Fix path separator on Windows.
677 entry_fixed = entry.replace('/', os.path.sep)
678 e_dir = os.path.join(self._root_dir, entry_fixed)
679 # Use entry and not entry_fixed there.
maruel@chromium.org0329e672009-05-13 18:41:04 +0000680 if entry not in entries and os.path.exists(e_dir):
msb@chromium.org83017012009-09-28 18:52:12 +0000681 modified_files = False
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000682 if isinstance(prev_entries, list):
msb@chromium.org83017012009-09-28 18:52:12 +0000683 # old .gclient_entries format was list, now dict
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000684 modified_files = gclient_scm.scm.SVN.CaptureStatus(e_dir)
msb@chromium.org83017012009-09-28 18:52:12 +0000685 else:
686 file_list = []
687 scm = gclient_scm.CreateSCM(prev_entries[entry], self._root_dir,
688 entry_fixed)
689 scm.status(self._options, [], file_list)
690 modified_files = file_list != []
691 if not self._options.delete_unversioned_trees or modified_files:
maruel@chromium.orgc5e9aec2009-08-03 18:25:56 +0000692 # There are modified files in this entry. Keep warning until
693 # removed.
maruel@chromium.orgc5e9aec2009-08-03 18:25:56 +0000694 print(("\nWARNING: \"%s\" is no longer part of this client. "
695 "It is recommended that you manually remove it.\n") %
696 entry_fixed)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000697 else:
698 # Delete the entry
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000699 print("\n________ deleting \'%s\' " +
maruel@chromium.orgc5e9aec2009-08-03 18:25:56 +0000700 "in \'%s\'") % (entry_fixed, self._root_dir)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000701 gclient_utils.RemoveDirectory(e_dir)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000702 # record the current list of entries for next time
703 self._SaveEntries(entries)
704
705 def PrintRevInfo(self):
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000706 """Output revision info mapping for the client and its dependencies.
707
708 This allows the capture of an overall "revision" for the source tree that
709 can be used to reproduce the same tree in the future. The actual output
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000710 contains enough information (source paths, svn server urls and revisions)
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000711 that it can be used either to generate external svn/git commands (without
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000712 gclient) or as input to gclient's --rev option (with some massaging of
713 the data).
714
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000715 Since we care about the revision of the current source tree, for git
716 repositories this command uses the revision of the HEAD. For subversion we
717 use BASE.
718
719 The --snapshot option allows creating a .gclient file to reproduce the tree.
720
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000721 Raises:
722 Error: If the client has conflicting entries.
723 """
724 # Check for revision overrides.
725 revision_overrides = {}
726 for revision in self._options.revisions:
727 if revision.find("@") < 0:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000728 raise gclient_utils.Error(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000729 "Specify the full dependency when specifying a revision number.")
730 revision_elem = revision.split("@")
731 # Disallow conflicting revs
732 if revision_overrides.has_key(revision_elem[0]) and \
733 revision_overrides[revision_elem[0]] != revision_elem[1]:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000734 raise gclient_utils.Error(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000735 "Conflicting revision numbers specified.")
736 revision_overrides[revision_elem[0]] = revision_elem[1]
737
738 solutions = self.GetVar("solutions")
739 if not solutions:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000740 raise gclient_utils.Error("No solution specified")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000741
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000742 # Inner helper to generate base url and rev tuple
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000743 def GetURLAndRev(name, original_url):
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000744 url, _ = gclient_utils.SplitUrlRevision(original_url)
745 scm = gclient_scm.CreateSCM(original_url, self._root_dir, name)
746 return (url, scm.revinfo(self._options, [], None))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000747
maruel@chromium.orge3da35f2010-03-09 21:40:45 +0000748 # text of the snapshot gclient file
749 new_gclient = ""
750 # Dictionary of { path : SCM url } to ensure no duplicate solutions
751 solution_names = {}
nasser@codeaurora.orgde8f3522010-03-11 23:47:44 +0000752 entries = {}
753 entries_deps_content = {}
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000754 # Run on the base solutions first.
755 for solution in solutions:
maruel@chromium.orge3da35f2010-03-09 21:40:45 +0000756 # Dictionary of { path : SCM url } to describe the gclient checkout
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000757 name = solution["name"]
maruel@chromium.orge3da35f2010-03-09 21:40:45 +0000758 if name in solution_names:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000759 raise gclient_utils.Error("solution %s specified more than once" % name)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000760 (url, rev) = GetURLAndRev(name, solution["url"])
msb@chromium.org770ff9e2009-09-23 17:18:18 +0000761 entries[name] = "%s@%s" % (url, rev)
maruel@chromium.orge3da35f2010-03-09 21:40:45 +0000762 solution_names[name] = "%s@%s" % (url, rev)
maruel@chromium.org1f7d1182010-05-17 18:17:38 +0000763 deps_file = solution.get("deps_file", DEPS_FILE)
nasser@codeaurora.org952d7c72010-03-01 20:41:01 +0000764 if '/' in deps_file or '\\' in deps_file:
765 raise gclient_utils.Error('deps_file name must not be a path, just a '
766 'filename.')
767 try:
768 deps_content = gclient_utils.FileRead(
769 os.path.join(self._root_dir, name, deps_file))
770 except IOError, e:
771 if e.errno != errno.ENOENT:
772 raise
773 deps_content = ""
774 entries_deps_content[name] = deps_content
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000775
nasser@codeaurora.orgde8f3522010-03-11 23:47:44 +0000776 # Process the dependencies next (sort alphanumerically to ensure that
777 # containing directories get populated first and for readability)
778 deps = self._ParseAllDeps(entries, entries_deps_content)
779 deps_to_process = deps.keys()
780 deps_to_process.sort()
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000781
nasser@codeaurora.orgde8f3522010-03-11 23:47:44 +0000782 # First pass for direct dependencies.
783 for d in deps_to_process:
784 if type(deps[d]) == str:
785 (url, rev) = GetURLAndRev(d, deps[d])
786 entries[d] = "%s@%s" % (url, rev)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000787
nasser@codeaurora.orgde8f3522010-03-11 23:47:44 +0000788 # Second pass for inherited deps (via the From keyword)
789 for d in deps_to_process:
tony@chromium.org4b5b1772010-04-08 01:52:56 +0000790 if isinstance(deps[d], self.FromImpl):
nasser@codeaurora.orgde8f3522010-03-11 23:47:44 +0000791 deps_parent_url = entries[deps[d].module_name]
792 if deps_parent_url.find("@") < 0:
793 raise gclient_utils.Error("From %s missing revisioned url" %
794 deps[d].module_name)
795 content = gclient_utils.FileRead(os.path.join(
796 self._root_dir,
797 deps[d].module_name,
maruel@chromium.org1f7d1182010-05-17 18:17:38 +0000798 DEPS_FILE))
nasser@codeaurora.orgde8f3522010-03-11 23:47:44 +0000799 sub_deps = self._ParseSolutionDeps(deps[d].module_name, content, {})
800 (url, rev) = GetURLAndRev(d, sub_deps[d])
801 entries[d] = "%s@%s" % (url, rev)
maruel@chromium.orge3da35f2010-03-09 21:40:45 +0000802
nasser@codeaurora.orgde8f3522010-03-11 23:47:44 +0000803 # Build the snapshot configuration string
804 if self._options.snapshot:
805 url = entries.pop(name)
806 custom_deps = ",\n ".join(["\"%s\": \"%s\"" % (x, entries[x])
807 for x in sorted(entries.keys())])
maruel@chromium.orge3da35f2010-03-09 21:40:45 +0000808
maruel@chromium.org1f7d1182010-05-17 18:17:38 +0000809 new_gclient += self.DEFAULT_SNAPSHOT_SOLUTION_TEXT % {
nasser@codeaurora.orgde8f3522010-03-11 23:47:44 +0000810 'solution_name': name,
811 'solution_url': url,
812 'safesync_url' : "",
813 'solution_deps': custom_deps,
814 }
815 else:
816 print(";\n".join(["%s: %s" % (x, entries[x])
817 for x in sorted(entries.keys())]))
maruel@chromium.orge3da35f2010-03-09 21:40:45 +0000818
819 # Print the snapshot configuration file
820 if self._options.snapshot:
821 config = DEFAULT_SNAPSHOT_FILE_TEXT % {'solution_list': new_gclient}
822 snapclient = GClient(self._root_dir, self._options)
823 snapclient.SetConfig(config)
824 print(snapclient._config_content)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000825
826
827## gclient commands.
828
829
maruel@chromium.org79692d62010-05-14 18:57:13 +0000830def CMDcleanup(options, args):
831 """Clean up all working copies, using 'svn cleanup' for each module.
832Additional options and args may be passed to 'svn cleanup'.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000833
maruel@chromium.org79692d62010-05-14 18:57:13 +0000834usage: cleanup [options] [--] [svn cleanup args/options]
835
836Valid options:
837 --verbose : output additional diagnostics
838"""
maruel@chromium.org2806acc2009-05-15 12:33:34 +0000839 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000840 if not client:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000841 raise gclient_utils.Error("client not configured; see 'gclient config'")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000842 if options.verbose:
843 # Print out the .gclient file. This is longer than if we just printed the
844 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000845 print(client.ConfigContent())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000846 return client.RunOnDeps('cleanup', args)
847
848
maruel@chromium.org79692d62010-05-14 18:57:13 +0000849def CMDconfig(options, args):
850 """Create a .gclient file in the current directory; this
851specifies the configuration for further commands. After update/sync,
852top-level DEPS files in each module are read to determine dependent
853modules to operate on as well. If optional [url] parameter is
854provided, then configuration is read from a specified Subversion server
855URL. Otherwise, a --spec option must be provided. A --name option overrides
856the default name for the solutions.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000857
maruel@chromium.org79692d62010-05-14 18:57:13 +0000858usage: config [option | url] [safesync url]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000859
maruel@chromium.org79692d62010-05-14 18:57:13 +0000860Valid options:
861 --name path : alternate relative path for the solution
862 --spec=GCLIENT_SPEC : contents of .gclient are read from string parameter.
863 *Note that due to Cygwin/Python brokenness, it
864 probably can't contain any newlines.*
865
866Examples:
867 gclient config https://gclient.googlecode.com/svn/trunk/gclient
868 configure a new client to check out gclient.py tool sources
869 gclient config --name tools https://gclient.googlecode.com/svn/trunk/gclient
870 gclient config --spec='solutions=[{"name":"gclient",
871 '"url":"https://gclient.googlecode.com/svn/trunk/gclient",'
872 '"custom_deps":{}}]'
873"""
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000874 if len(args) < 1 and not options.spec:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000875 raise gclient_utils.Error("required argument missing; see 'gclient help "
876 "config'")
maruel@chromium.org0329e672009-05-13 18:41:04 +0000877 if os.path.exists(options.config_filename):
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000878 raise gclient_utils.Error("%s file already exists in the current directory"
879 % options.config_filename)
maruel@chromium.org2806acc2009-05-15 12:33:34 +0000880 client = GClient('.', options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000881 if options.spec:
882 client.SetConfig(options.spec)
883 else:
maruel@chromium.org1ab7ffc2009-06-03 17:21:37 +0000884 base_url = args[0].rstrip('/')
iposva@chromium.org8cf7a392010-04-07 17:20:26 +0000885 if not options.name:
886 name = base_url.split("/")[-1]
887 else:
888 # specify an alternate relpath for the given URL.
889 name = options.name
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000890 safesync_url = ""
891 if len(args) > 1:
892 safesync_url = args[1]
893 client.SetDefaultConfig(name, base_url, safesync_url)
894 client.SaveConfig()
maruel@chromium.org79692d62010-05-14 18:57:13 +0000895 return 0
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000896
897
maruel@chromium.org79692d62010-05-14 18:57:13 +0000898def CMDexport(options, args):
899 """Wrapper for svn export for all managed directories
900"""
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +0000901 if len(args) != 1:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000902 raise gclient_utils.Error("Need directory name")
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +0000903 client = GClient.LoadCurrentConfig(options)
904
905 if not client:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000906 raise gclient_utils.Error("client not configured; see 'gclient config'")
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +0000907
908 if options.verbose:
909 # Print out the .gclient file. This is longer than if we just printed the
910 # client dict, but more legible, and it might contain helpful comments.
911 print(client.ConfigContent())
912 return client.RunOnDeps('export', args)
913
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000914
maruel@chromium.org79692d62010-05-14 18:57:13 +0000915def CMDhelp(options, args):
916 """Describe the usage of this program or its subcommands.
917
918usage: help [options] [subcommand]
919
920Valid options:
921 --verbose : output additional diagnostics
922"""
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000923 __pychecker__ = 'unusednames=options'
maruel@chromium.org79692d62010-05-14 18:57:13 +0000924 module = sys.modules[__name__]
925 commands = [x[3:] for x in dir(module) if x.startswith('CMD')]
926 if len(args) == 1 and args[0] in commands:
927 print getattr(module, 'CMD' + args[0]).__doc__
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000928 else:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000929 raise gclient_utils.Error("unknown subcommand '%s'; see 'gclient help'" %
930 args[0])
maruel@chromium.org79692d62010-05-14 18:57:13 +0000931 return 0
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000932
933
maruel@chromium.org79692d62010-05-14 18:57:13 +0000934def CMDpack(options, args):
935 """Generate a patch which can be applied at the root of the tree.
936Internally, runs 'svn diff' on each checked out module and
937dependencies, and performs minimal postprocessing of the output. The
938resulting patch is printed to stdout and can be applied to a freshly
939checked out tree via 'patch -p0 < patchfile'. Additional args and
940options to 'svn diff' can be passed after gclient options.
kbr@google.comab318592009-09-04 00:54:55 +0000941
maruel@chromium.org79692d62010-05-14 18:57:13 +0000942usage: pack [options] [--] [svn args/options]
943
944Valid options:
945 --verbose : output additional diagnostics
946
947Examples:
948 gclient pack > patch.txt
949 generate simple patch for configured client and dependences
950 gclient pack -- -x -b > patch.txt
951 generate patch using 'svn diff -x -b' to suppress
952 whitespace-only differences
953 gclient pack -- -r HEAD -x -b > patch.txt
954 generate patch, diffing each file versus the latest version of
955 each module
956"""
kbr@google.comab318592009-09-04 00:54:55 +0000957 client = GClient.LoadCurrentConfig(options)
958 if not client:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000959 raise gclient_utils.Error("client not configured; see 'gclient config'")
kbr@google.comab318592009-09-04 00:54:55 +0000960 if options.verbose:
961 # Print out the .gclient file. This is longer than if we just printed the
962 # client dict, but more legible, and it might contain helpful comments.
963 print(client.ConfigContent())
kbr@google.comab318592009-09-04 00:54:55 +0000964 return client.RunOnDeps('pack', args)
965
966
maruel@chromium.org79692d62010-05-14 18:57:13 +0000967def CMDstatus(options, args):
968 """Show the status of client and dependent modules, using 'svn diff'
969for each module. Additional options and args may be passed to 'svn diff'.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000970
maruel@chromium.org79692d62010-05-14 18:57:13 +0000971usage: status [options] [--] [svn diff args/options]
972
973Valid options:
974 --verbose : output additional diagnostics
975 --nohooks : don't run the hooks after the update is complete
976"""
maruel@chromium.org2806acc2009-05-15 12:33:34 +0000977 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000978 if not client:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000979 raise gclient_utils.Error("client not configured; see 'gclient config'")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000980 if options.verbose:
981 # Print out the .gclient file. This is longer than if we just printed the
982 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000983 print(client.ConfigContent())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000984 return client.RunOnDeps('status', args)
985
986
maruel@chromium.org79692d62010-05-14 18:57:13 +0000987def CMDsync(options, args):
988 """Perform a checkout/update of the modules specified by the gclient
989configuration; see 'help config'. Unless --revision is specified,
990then the latest revision of the root solutions is checked out, with
991dependent submodule versions updated according to DEPS files.
992If --revision is specified, then the given revision is used in place
993of the latest, either for a single solution or for all solutions.
994Unless the --force option is provided, solutions and modules whose
995local revision matches the one to update (i.e., they have not changed
996in the repository) are *not* modified. Unless --nohooks is provided,
997the hooks are run.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000998
maruel@chromium.org79692d62010-05-14 18:57:13 +0000999usage: gclient sync [options] [--] [SCM update options/args]
1000
1001Valid options:
1002 --force : force update even for unchanged modules
1003 --nohooks : don't run the hooks after the update is complete
1004 --revision SOLUTION@REV : update given solution to specified revision
1005 --deps PLATFORM(S) : sync deps for the given platform(s), or 'all'
1006 --verbose : output additional diagnostics
1007 --head : update to latest revision, instead of last good
1008 revision
1009 --reset : resets any local changes before updating (git only)
1010
1011Examples:
1012 gclient sync
1013 update files from SCM according to current configuration,
1014 *for modules which have changed since last update or sync*
1015 gclient sync --force
1016 update files from SCM according to current configuration, for
1017 all modules (useful for recovering files deleted from local copy)
1018 gclient sync --revision src@31000
1019 update src directory to r31000
1020"""
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001021 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001022
1023 if not client:
maruel@chromium.orge3608df2009-11-10 20:22:57 +00001024 raise gclient_utils.Error("client not configured; see 'gclient config'")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001025
1026 if not options.head:
1027 solutions = client.GetVar('solutions')
1028 if solutions:
1029 for s in solutions:
1030 if s.get('safesync_url', ''):
1031 # rip through revisions and make sure we're not over-riding
1032 # something that was explicitly passed
1033 has_key = False
1034 for r in options.revisions:
1035 if r.split('@')[0] == s['name']:
1036 has_key = True
1037 break
1038
1039 if not has_key:
1040 handle = urllib.urlopen(s['safesync_url'])
1041 rev = handle.read().strip()
1042 handle.close()
1043 if len(rev):
1044 options.revisions.append(s['name']+'@'+rev)
1045
1046 if options.verbose:
1047 # Print out the .gclient file. This is longer than if we just printed the
1048 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +00001049 print(client.ConfigContent())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001050 return client.RunOnDeps('update', args)
1051
1052
maruel@chromium.org79692d62010-05-14 18:57:13 +00001053def CMDupdate(options, args):
1054 """Alias for the sync command. Deprecated.
1055"""
1056 return CMDsync(options, args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001057
maruel@chromium.org79692d62010-05-14 18:57:13 +00001058
1059def CMDdiff(options, args):
1060 """Display the differences between two revisions of modules.
1061(Does 'svn diff' for each checked out module and dependences.)
1062Additional args and options to 'svn diff' can be passed after
1063gclient options.
1064
1065usage: diff [options] [--] [svn args/options]
1066
1067Valid options:
1068 --verbose : output additional diagnostics
1069
1070Examples:
1071 gclient diff
1072 simple 'svn diff' for configured client and dependences
1073 gclient diff -- -x -b
1074 use 'svn diff -x -b' to suppress whitespace-only differences
1075 gclient diff -- -r HEAD -x -b
1076 diff versus the latest version of each module
1077"""
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 if options.verbose:
1082 # Print out the .gclient file. This is longer than if we just printed the
1083 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +00001084 print(client.ConfigContent())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001085 return client.RunOnDeps('diff', args)
1086
1087
maruel@chromium.org79692d62010-05-14 18:57:13 +00001088def CMDrevert(options, args):
1089 """Revert every file in every managed directory in the client view.
1090"""
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001091 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001092 if not client:
maruel@chromium.orge3608df2009-11-10 20:22:57 +00001093 raise gclient_utils.Error("client not configured; see 'gclient config'")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001094 return client.RunOnDeps('revert', args)
1095
1096
maruel@chromium.org79692d62010-05-14 18:57:13 +00001097def CMDrunhooks(options, args):
1098 """Runs hooks for files that have been modified in the local working copy,
1099according to 'svn status'. Implies --force.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001100
maruel@chromium.org79692d62010-05-14 18:57:13 +00001101usage: runhooks [options]
1102
1103Valid options:
1104 --verbose : output additional diagnostics
1105"""
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001106 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001107 if not client:
maruel@chromium.orge3608df2009-11-10 20:22:57 +00001108 raise gclient_utils.Error("client not configured; see 'gclient config'")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001109 if options.verbose:
1110 # Print out the .gclient file. This is longer than if we just printed the
1111 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +00001112 print(client.ConfigContent())
maruel@chromium.org5df6a462009-08-28 18:52:26 +00001113 options.force = True
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001114 return client.RunOnDeps('runhooks', args)
1115
1116
maruel@chromium.org79692d62010-05-14 18:57:13 +00001117def CMDrevinfo(options, args):
1118 """Outputs source path, server URL and revision information for every
1119dependency in all solutions.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001120
maruel@chromium.org79692d62010-05-14 18:57:13 +00001121usage: revinfo [options]
1122"""
maruel@chromium.orge3608df2009-11-10 20:22:57 +00001123 __pychecker__ = 'unusednames=args'
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001124 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001125 if not client:
maruel@chromium.orge3608df2009-11-10 20:22:57 +00001126 raise gclient_utils.Error("client not configured; see 'gclient config'")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001127 client.PrintRevInfo()
maruel@chromium.org79692d62010-05-14 18:57:13 +00001128 return 0
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001129
1130
maruel@chromium.org79692d62010-05-14 18:57:13 +00001131def DispatchCommand(command, options, args):
1132 """Dispatches the appropriate subcommand based on command line arguments.
1133"""
1134 module = sys.modules[__name__]
1135 command = getattr(module, 'CMD' + command, None)
1136 if command:
1137 return command(options, args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001138 else:
maruel@chromium.orge3608df2009-11-10 20:22:57 +00001139 raise gclient_utils.Error("unknown subcommand '%s'; see 'gclient help'" %
1140 command)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001141
1142
1143def Main(argv):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001144 option_parser = optparse.OptionParser(usage=DEFAULT_USAGE_TEXT,
1145 version=__version__)
maruel@chromium.orge9e22232010-05-16 23:48:58 +00001146 option_parser.add_option("--force", action="store_true",
1147 help="(update/sync only) force update even "
1148 "for modules which haven't changed")
1149 option_parser.add_option("--nohooks", action="store_true",
1150 help="(update/sync/revert only) prevent the hooks "
1151 "from running")
1152 option_parser.add_option("--revision", action="append", dest="revisions",
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001153 metavar="REV", default=[],
maruel@chromium.orge9e22232010-05-16 23:48:58 +00001154 help="(update/sync only) sync to a specific "
1155 "revision, can be used multiple times for "
1156 "each solution, e.g. --revision=src@123, "
1157 "--revision=internal@32")
1158 option_parser.add_option("--deps", dest="deps_os", metavar="OS_LIST",
1159 help="(update/sync only) sync deps for the "
1160 "specified (comma-separated) platform(s); "
1161 "'all' will sync all platforms")
1162 option_parser.add_option("--reset", action="store_true",
1163 help="(update/sync only) resets any local changes "
1164 "before updating (git only)")
1165 option_parser.add_option("--spec",
1166 help="(config only) create a gclient file "
1167 "containing the provided string")
maruel@chromium.orga6220d12010-01-06 21:04:17 +00001168 option_parser.add_option("-v", "--verbose", action="count", default=0,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001169 help="produce additional output for diagnostics")
maruel@chromium.orge9e22232010-05-16 23:48:58 +00001170 option_parser.add_option("--manually_grab_svn_rev", action="store_true",
maruel@chromium.org7753d242009-10-07 17:40:24 +00001171 help="Skip svn up whenever possible by requesting "
1172 "actual HEAD revision from the repository")
maruel@chromium.orge9e22232010-05-16 23:48:58 +00001173 option_parser.add_option("--head", action="store_true",
1174 help="skips any safesync_urls specified in "
1175 "configured solutions")
1176 option_parser.add_option("--delete_unversioned_trees", action="store_true",
1177 help="on update, delete any unexpected "
1178 "unversioned trees that are in the checkout")
1179 option_parser.add_option("--snapshot", action="store_true",
1180 help="(revinfo only), create a snapshot file "
1181 "of the current version of all repositories")
1182 option_parser.add_option("--name",
iposva@chromium.org8cf7a392010-04-07 17:20:26 +00001183 help="specify alternate relative solution path")
maruel@chromium.orge9e22232010-05-16 23:48:58 +00001184 option_parser.add_option("--gclientfile", metavar="FILENAME",
1185 help="specify an alternate .gclient file")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001186
1187 if len(argv) < 2:
1188 # Users don't need to be told to use the 'help' command.
1189 option_parser.print_help()
1190 return 1
1191 # Add manual support for --version as first argument.
1192 if argv[1] == '--version':
1193 option_parser.print_version()
1194 return 0
1195
1196 # Add manual support for --help as first argument.
1197 if argv[1] == '--help':
1198 argv[1] = 'help'
1199
1200 command = argv[1]
1201 options, args = option_parser.parse_args(argv[2:])
1202
1203 if len(argv) < 3 and command == "help":
1204 option_parser.print_help()
1205 return 0
1206
maruel@chromium.orga6220d12010-01-06 21:04:17 +00001207 if options.verbose > 1:
maruel@chromium.org754960e2009-09-21 12:31:05 +00001208 logging.basicConfig(level=logging.DEBUG)
1209
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001210 # Files used for configuration and state saving.
1211 options.config_filename = os.environ.get("GCLIENT_FILE", ".gclient")
maruel@chromium.orge3da35f2010-03-09 21:40:45 +00001212 if options.gclientfile:
1213 options.config_filename = options.gclientfile
1214 options.entries_filename = options.config_filename + "_entries"
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001215 options.deps_file = "DEPS"
1216
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001217 options.platform = sys.platform
1218 return DispatchCommand(command, options, args)
1219
1220
1221if "__main__" == __name__:
1222 try:
1223 result = Main(sys.argv)
maruel@chromium.orge3608df2009-11-10 20:22:57 +00001224 except gclient_utils.Error, e:
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +00001225 print >> sys.stderr, "Error: %s" % str(e)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001226 result = 1
1227 sys.exit(result)
1228
1229# vim: ts=2:sw=2:tw=80:et: