blob: e7672a14b5de59ba8b63e08b00f9cab7a64b1e5a [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.orgddff62d2010-05-17 21:02:36 +000058__version__ = "0.3.7"
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 = (
maruel@chromium.orgddff62d2010-05-17 21:02:36 +000078"""%prog <subcommand> [options] [--] [SCM options/args...]
msb@chromium.orgd6504212010-01-13 17:34:31 +000079a wrapper for managing a set of svn client modules and/or git repositories.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000080Version """ + __version__ + """
81
msb@chromium.orgd6504212010-01-13 17:34:31 +000082Options and extra arguments can be passed to invoked SCM commands by
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000083appending them to the command line. Note that if the first such
84appended option starts with a dash (-) then the options must be
85preceded by -- to distinguish them from gclient options.
86
87For additional help on a subcommand or examples of usage, try
88 %prog help <subcommand>
89 %prog help files
90""")
91
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000092
maruel@chromium.org1f7d1182010-05-17 18:17:38 +000093def attr(attr, data):
94 """Sets an attribute on a function."""
95 def hook(fn):
96 setattr(fn, attr, data)
97 return fn
98 return hook
maruel@chromium.orge3da35f2010-03-09 21:40:45 +000099
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000100
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000101## GClient implementation.
102
103
104class GClient(object):
105 """Object that represent a gclient checkout."""
106
107 supported_commands = [
kbr@google.comab318592009-09-04 00:54:55 +0000108 'cleanup', 'diff', 'export', 'pack', 'revert', 'status', 'update',
109 'runhooks'
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000110 ]
111
maruel@chromium.org491c04b2010-05-17 18:17:44 +0000112 deps_os_choices = {
113 "win32": "win",
114 "win": "win",
115 "cygwin": "win",
116 "darwin": "mac",
117 "mac": "mac",
118 "unix": "unix",
119 "linux": "unix",
120 "linux2": "unix",
121 }
122
maruel@chromium.org1f7d1182010-05-17 18:17:38 +0000123 DEPS_FILE = 'DEPS'
124
125 DEFAULT_CLIENT_FILE_TEXT = ("""\
126solutions = [
127 { "name" : "%(solution_name)s",
128 "url" : "%(solution_url)s",
129 "custom_deps" : {
130 },
131 "safesync_url": "%(safesync_url)s"
132 },
133]
134""")
135
136 DEFAULT_SNAPSHOT_SOLUTION_TEXT = ("""\
137 { "name" : "%(solution_name)s",
138 "url" : "%(solution_url)s",
139 "custom_deps" : {
140 %(solution_deps)s,
141 },
142 "safesync_url": "%(safesync_url)s"
143 },
144""")
145
146 DEFAULT_SNAPSHOT_FILE_TEXT = ("""\
147# Snapshot generated with gclient revinfo --snapshot
148solutions = [
149%(solution_list)s
150]
151""")
152
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000153 def __init__(self, root_dir, options):
154 self._root_dir = root_dir
155 self._options = options
156 self._config_content = None
157 self._config_dict = {}
158 self._deps_hooks = []
159
160 def SetConfig(self, content):
161 self._config_dict = {}
162 self._config_content = content
skylined@chromium.orgdf0032c2009-05-29 10:43:56 +0000163 try:
164 exec(content, self._config_dict)
165 except SyntaxError, e:
166 try:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000167 __pychecker__ = 'no-objattrs'
skylined@chromium.orgdf0032c2009-05-29 10:43:56 +0000168 # Try to construct a human readable error message
169 error_message = [
170 'There is a syntax error in your configuration file.',
171 'Line #%s, character %s:' % (e.lineno, e.offset),
172 '"%s"' % re.sub(r'[\r\n]*$', '', e.text) ]
173 except:
174 # Something went wrong, re-raise the original exception
175 raise e
176 else:
177 # Raise a new exception with the human readable message:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000178 raise gclient_utils.Error('\n'.join(error_message))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000179
180 def SaveConfig(self):
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000181 gclient_utils.FileWrite(os.path.join(self._root_dir,
182 self._options.config_filename),
183 self._config_content)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000184
185 def _LoadConfig(self):
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000186 client_source = gclient_utils.FileRead(
187 os.path.join(self._root_dir, self._options.config_filename))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000188 self.SetConfig(client_source)
189
190 def ConfigContent(self):
191 return self._config_content
192
193 def GetVar(self, key, default=None):
194 return self._config_dict.get(key, default)
195
196 @staticmethod
197 def LoadCurrentConfig(options, from_dir=None):
198 """Searches for and loads a .gclient file relative to the current working
199 dir.
200
201 Returns:
202 A dict representing the contents of the .gclient file or an empty dict if
203 the .gclient file doesn't exist.
204 """
205 if not from_dir:
206 from_dir = os.curdir
207 path = os.path.realpath(from_dir)
maruel@chromium.org0329e672009-05-13 18:41:04 +0000208 while not os.path.exists(os.path.join(path, options.config_filename)):
maruel@chromium.org55e724e2010-03-11 19:36:49 +0000209 split_path = os.path.split(path)
210 if not split_path[1]:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000211 return None
maruel@chromium.org55e724e2010-03-11 19:36:49 +0000212 path = split_path[0]
maruel@chromium.org2806acc2009-05-15 12:33:34 +0000213 client = GClient(path, options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000214 client._LoadConfig()
215 return client
216
217 def SetDefaultConfig(self, solution_name, solution_url, safesync_url):
maruel@chromium.org1f7d1182010-05-17 18:17:38 +0000218 self.SetConfig(self.DEFAULT_CLIENT_FILE_TEXT % {
gspencer@google.comdf2d5902009-09-11 22:16:21 +0000219 'solution_name': solution_name,
220 'solution_url': solution_url,
221 'safesync_url' : safesync_url,
222 })
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000223
224 def _SaveEntries(self, entries):
225 """Creates a .gclient_entries file to record the list of unique checkouts.
226
227 The .gclient_entries file lives in the same directory as .gclient.
228
229 Args:
230 entries: A sequence of solution names.
231 """
maruel@chromium.orge41f6822010-04-08 16:37:06 +0000232 # Sometimes pprint.pformat will use {', sometimes it'll use { ' ... It
233 # makes testing a bit too fun.
234 result = pprint.pformat(entries, 2)
235 if result.startswith('{\''):
maruel@chromium.org1edec4d2010-04-08 17:17:06 +0000236 result = '{ \'' + result[2:]
maruel@chromium.orge41f6822010-04-08 16:37:06 +0000237 text = "entries = \\\n" + result + '\n'
msb@chromium.org2e38de72009-09-28 17:04:47 +0000238 file_path = os.path.join(self._root_dir, self._options.entries_filename)
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000239 gclient_utils.FileWrite(file_path, text)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000240
241 def _ReadEntries(self):
242 """Read the .gclient_entries file for the given client.
243
244 Args:
245 client: The client for which the entries file should be read.
246
247 Returns:
248 A sequence of solution names, which will be empty if there is the
249 entries file hasn't been created yet.
250 """
251 scope = {}
252 filename = os.path.join(self._root_dir, self._options.entries_filename)
maruel@chromium.org0329e672009-05-13 18:41:04 +0000253 if not os.path.exists(filename):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000254 return []
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000255 exec(gclient_utils.FileRead(filename), scope)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000256 return scope["entries"]
257
258 class FromImpl:
259 """Used to implement the From syntax."""
260
tony@chromium.org30ef9ae2010-04-09 02:18:05 +0000261 def __init__(self, module_name, sub_target_name=None):
262 """module_name is the dep module we want to include from. It can also be
263 the name of a subdirectory to include from.
264
265 sub_target_name is an optional parameter if the module name in the other
266 DEPS file is different. E.g., you might want to map src/net to net."""
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000267 self.module_name = module_name
tony@chromium.org30ef9ae2010-04-09 02:18:05 +0000268 self.sub_target_name = sub_target_name
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000269
270 def __str__(self):
tony@chromium.org30ef9ae2010-04-09 02:18:05 +0000271 return 'From(%s, %s)' % (repr(self.module_name),
272 repr(self.sub_target_name))
273
274 def GetUrl(self, target_name, sub_deps_base_url, root_dir, sub_deps):
275 """Resolve the URL for this From entry."""
276 sub_deps_target_name = target_name
277 if self.sub_target_name:
278 sub_deps_target_name = self.sub_target_name
279 url = sub_deps[sub_deps_target_name]
280 if url.startswith('/'):
281 # If it's a relative URL, we need to resolve the URL relative to the
282 # sub deps base URL.
283 if not isinstance(sub_deps_base_url, basestring):
284 sub_deps_base_url = sub_deps_base_url.GetPath()
285 scm = gclient_scm.CreateSCM(sub_deps_base_url, root_dir,
286 None)
287 url = scm.FullUrlForRelativeUrl(url)
288 return url
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000289
tony@chromium.org4b5b1772010-04-08 01:52:56 +0000290 class FileImpl:
291 """Used to implement the File('') syntax which lets you sync a single file
292 from an SVN repo."""
293
294 def __init__(self, file_location):
295 self.file_location = file_location
296
297 def __str__(self):
298 return 'File("%s")' % self.file_location
299
300 def GetPath(self):
301 return os.path.split(self.file_location)[0]
302
303 def GetFilename(self):
304 rev_tokens = self.file_location.split('@')
305 return os.path.split(rev_tokens[0])[1]
306
307 def GetRevision(self):
308 rev_tokens = self.file_location.split('@')
309 if len(rev_tokens) > 1:
310 return rev_tokens[1]
311 return None
312
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000313 class _VarImpl:
314 def __init__(self, custom_vars, local_scope):
315 self._custom_vars = custom_vars
316 self._local_scope = local_scope
317
318 def Lookup(self, var_name):
319 """Implements the Var syntax."""
320 if var_name in self._custom_vars:
321 return self._custom_vars[var_name]
322 elif var_name in self._local_scope.get("vars", {}):
323 return self._local_scope["vars"][var_name]
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000324 raise gclient_utils.Error("Var is not defined: %s" % var_name)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000325
326 def _ParseSolutionDeps(self, solution_name, solution_deps_content,
tony@chromium.org30ef9ae2010-04-09 02:18:05 +0000327 custom_vars, parse_hooks):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000328 """Parses the DEPS file for the specified solution.
329
330 Args:
331 solution_name: The name of the solution to query.
332 solution_deps_content: Content of the DEPS file for the solution
333 custom_vars: A dict of vars to override any vars defined in the DEPS file.
334
335 Returns:
336 A dict mapping module names (as relative paths) to URLs or an empty
337 dict if the solution does not have a DEPS file.
338 """
339 # Skip empty
340 if not solution_deps_content:
341 return {}
342 # Eval the content
343 local_scope = {}
344 var = self._VarImpl(custom_vars, local_scope)
tony@chromium.org4b5b1772010-04-08 01:52:56 +0000345 global_scope = {
346 "File": self.FileImpl,
347 "From": self.FromImpl,
348 "Var": var.Lookup,
349 "deps_os": {},
350 }
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000351 exec(solution_deps_content, global_scope, local_scope)
352 deps = local_scope.get("deps", {})
353
354 # load os specific dependencies if defined. these dependencies may
355 # override or extend the values defined by the 'deps' member.
356 if "deps_os" in local_scope:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000357
358 if self._options.deps_os is not None:
359 deps_to_include = self._options.deps_os.split(",")
360 if "all" in deps_to_include:
maruel@chromium.org491c04b2010-05-17 18:17:44 +0000361 deps_to_include = list(set(self.deps_os_choices.itervalues()))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000362 else:
maruel@chromium.org491c04b2010-05-17 18:17:44 +0000363 deps_to_include = [self.deps_os_choices.get(sys.platform, "unix")]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000364
365 deps_to_include = set(deps_to_include)
366 for deps_os_key in deps_to_include:
367 os_deps = local_scope["deps_os"].get(deps_os_key, {})
368 if len(deps_to_include) > 1:
369 # Ignore any overrides when including deps for more than one
370 # platform, so we collect the broadest set of dependencies available.
371 # We may end up with the wrong revision of something for our
372 # platform, but this is the best we can do.
373 deps.update([x for x in os_deps.items() if not x[0] in deps])
374 else:
375 deps.update(os_deps)
376
tony@chromium.org30ef9ae2010-04-09 02:18:05 +0000377 if 'hooks' in local_scope and parse_hooks:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000378 self._deps_hooks.extend(local_scope['hooks'])
379
380 # If use_relative_paths is set in the DEPS file, regenerate
381 # the dictionary using paths relative to the directory containing
382 # the DEPS file.
383 if local_scope.get('use_relative_paths'):
384 rel_deps = {}
385 for d, url in deps.items():
386 # normpath is required to allow DEPS to use .. in their
387 # dependency local path.
388 rel_deps[os.path.normpath(os.path.join(solution_name, d))] = url
389 return rel_deps
390 else:
391 return deps
392
393 def _ParseAllDeps(self, solution_urls, solution_deps_content):
394 """Parse the complete list of dependencies for the client.
395
396 Args:
397 solution_urls: A dict mapping module names (as relative paths) to URLs
398 corresponding to the solutions specified by the client. This parameter
399 is passed as an optimization.
400 solution_deps_content: A dict mapping module names to the content
401 of their DEPS files
402
403 Returns:
404 A dict mapping module names (as relative paths) to URLs corresponding
405 to the entire set of dependencies to checkout for the given client.
406
407 Raises:
408 Error: If a dependency conflicts with another dependency or of a solution.
409 """
410 deps = {}
411 for solution in self.GetVar("solutions"):
412 custom_vars = solution.get("custom_vars", {})
413 solution_deps = self._ParseSolutionDeps(
414 solution["name"],
415 solution_deps_content[solution["name"]],
tony@chromium.org30ef9ae2010-04-09 02:18:05 +0000416 custom_vars,
417 True)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000418
419 # If a line is in custom_deps, but not in the solution, we want to append
420 # this line to the solution.
421 if "custom_deps" in solution:
422 for d in solution["custom_deps"]:
423 if d not in solution_deps:
424 solution_deps[d] = solution["custom_deps"][d]
425
426 for d in solution_deps:
427 if "custom_deps" in solution and d in solution["custom_deps"]:
428 # Dependency is overriden.
429 url = solution["custom_deps"][d]
430 if url is None:
431 continue
432 else:
433 url = solution_deps[d]
434 # if we have a From reference dependent on another solution, then
435 # just skip the From reference. When we pull deps for the solution,
436 # we will take care of this dependency.
437 #
438 # If multiple solutions all have the same From reference, then we
439 # should only add one to our list of dependencies.
tony@chromium.org4b5b1772010-04-08 01:52:56 +0000440 if isinstance(url, self.FromImpl):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000441 if url.module_name in solution_urls:
442 # Already parsed.
443 continue
444 if d in deps and type(deps[d]) != str:
445 if url.module_name == deps[d].module_name:
446 continue
tony@chromium.org4b5b1772010-04-08 01:52:56 +0000447 elif isinstance(url, str):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000448 parsed_url = urlparse.urlparse(url)
449 scheme = parsed_url[0]
450 if not scheme:
451 # A relative url. Fetch the real base.
452 path = parsed_url[2]
453 if path[0] != "/":
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000454 raise gclient_utils.Error(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000455 "relative DEPS entry \"%s\" must begin with a slash" % d)
msb@chromium.orge6f78352010-01-13 17:05:33 +0000456 # Create a scm just to query the full url.
457 scm = gclient_scm.CreateSCM(solution["url"], self._root_dir,
458 None)
459 url = scm.FullUrlForRelativeUrl(url)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000460 if d in deps and deps[d] != url:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000461 raise gclient_utils.Error(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000462 "Solutions have conflicting versions of dependency \"%s\"" % d)
463 if d in solution_urls and solution_urls[d] != url:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000464 raise gclient_utils.Error(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000465 "Dependency \"%s\" conflicts with specified solution" % d)
466 # Grab the dependency.
467 deps[d] = url
468 return deps
469
phajdan.jr@chromium.org71b40682009-07-31 23:40:09 +0000470 def _RunHookAction(self, hook_dict, matching_file_list):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000471 """Runs the action from a single hook.
472 """
473 command = hook_dict['action'][:]
474 if command[0] == 'python':
475 # If the hook specified "python" as the first item, the action is a
476 # Python script. Run it by starting a new copy of the same
477 # interpreter.
478 command[0] = sys.executable
479
phajdan.jr@chromium.org71b40682009-07-31 23:40:09 +0000480 if '$matching_files' in command:
phajdan.jr@chromium.org68f2e092009-08-06 17:05:35 +0000481 splice_index = command.index('$matching_files')
482 command[splice_index:splice_index + 1] = matching_file_list
phajdan.jr@chromium.org71b40682009-07-31 23:40:09 +0000483
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000484 # Use a discrete exit status code of 2 to indicate that a hook action
485 # failed. Users of this script may wish to treat hook action failures
486 # differently from VC failures.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000487 gclient_utils.SubprocessCall(command, self._root_dir, fail_status=2)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000488
489 def _RunHooks(self, command, file_list, is_using_git):
490 """Evaluates all hooks, running actions as needed.
491 """
492 # Hooks only run for these command types.
493 if not command in ('update', 'revert', 'runhooks'):
494 return
495
evan@chromium.org67820ef2009-07-27 17:23:00 +0000496 # Hooks only run when --nohooks is not specified
497 if self._options.nohooks:
498 return
499
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000500 # Get any hooks from the .gclient file.
501 hooks = self.GetVar("hooks", [])
502 # Add any hooks found in DEPS files.
503 hooks.extend(self._deps_hooks)
504
505 # If "--force" was specified, run all hooks regardless of what files have
506 # changed. If the user is using git, then we don't know what files have
507 # changed so we always run all hooks.
508 if self._options.force or is_using_git:
509 for hook_dict in hooks:
phajdan.jr@chromium.org71b40682009-07-31 23:40:09 +0000510 self._RunHookAction(hook_dict, [])
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000511 return
512
513 # Run hooks on the basis of whether the files from the gclient operation
514 # match each hook's pattern.
515 for hook_dict in hooks:
516 pattern = re.compile(hook_dict['pattern'])
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000517 matching_file_list = [f for f in file_list if pattern.search(f)]
phajdan.jr@chromium.org71b40682009-07-31 23:40:09 +0000518 if matching_file_list:
519 self._RunHookAction(hook_dict, matching_file_list)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000520
521 def RunOnDeps(self, command, args):
522 """Runs a command on each dependency in a client and its dependencies.
523
524 The module's dependencies are specified in its top-level DEPS files.
525
526 Args:
527 command: The command to use (e.g., 'status' or 'diff')
528 args: list of str - extra arguments to add to the command line.
529
530 Raises:
531 Error: If the client has conflicting entries.
532 """
533 if not command in self.supported_commands:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000534 raise gclient_utils.Error("'%s' is an unsupported command" % command)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000535
536 # Check for revision overrides.
537 revision_overrides = {}
538 for revision in self._options.revisions:
539 if revision.find("@") == -1:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000540 raise gclient_utils.Error(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000541 "Specify the full dependency when specifying a revision number.")
542 revision_elem = revision.split("@")
543 # Disallow conflicting revs
544 if revision_overrides.has_key(revision_elem[0]) and \
545 revision_overrides[revision_elem[0]] != revision_elem[1]:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000546 raise gclient_utils.Error(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000547 "Conflicting revision numbers specified.")
548 revision_overrides[revision_elem[0]] = revision_elem[1]
549
550 solutions = self.GetVar("solutions")
551 if not solutions:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000552 raise gclient_utils.Error("No solution specified")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000553
554 # When running runhooks --force, there's no need to consult the SCM.
555 # All known hooks are expected to run unconditionally regardless of working
556 # copy state, so skip the SCM status check.
557 run_scm = not (command == 'runhooks' and self._options.force)
558
559 entries = {}
560 entries_deps_content = {}
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000561 file_list = []
562 # Run on the base solutions first.
563 for solution in solutions:
564 name = solution["name"]
maruel@chromium.org1f7d1182010-05-17 18:17:38 +0000565 deps_file = solution.get("deps_file", self.DEPS_FILE)
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000566 if '/' in deps_file or '\\' in deps_file:
567 raise gclient_utils.Error('deps_file name must not be a path, just a '
568 'filename.')
569 if name in entries:
570 raise gclient_utils.Error("solution %s specified more than once" % name)
571 url = solution["url"]
572 entries[name] = url
573 if run_scm and url:
574 self._options.revision = revision_overrides.get(name)
575 scm = gclient_scm.CreateSCM(url, self._root_dir, name)
576 scm.RunCommand(command, self._options, args, file_list)
577 file_list = [os.path.join(name, f.strip()) for f in file_list]
578 self._options.revision = None
579 try:
580 deps_content = gclient_utils.FileRead(
581 os.path.join(self._root_dir, name, deps_file))
582 except IOError, e:
583 if e.errno != errno.ENOENT:
584 raise
585 deps_content = ""
586 entries_deps_content[name] = deps_content
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000587
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000588 # Process the dependencies next (sort alphanumerically to ensure that
589 # containing directories get populated first and for readability)
590 deps = self._ParseAllDeps(entries, entries_deps_content)
591 deps_to_process = deps.keys()
592 deps_to_process.sort()
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000593
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000594 # First pass for direct dependencies.
595 if command == 'update' and not self._options.verbose:
596 pm = Progress('Syncing projects', len(deps_to_process))
597 for d in deps_to_process:
nasser@codeaurora.org1f7a3d12010-02-04 15:11:50 +0000598 if command == 'update' and not self._options.verbose:
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000599 pm.update()
600 if type(deps[d]) == str:
601 url = deps[d]
602 entries[d] = url
603 if run_scm:
604 self._options.revision = revision_overrides.get(d)
605 scm = gclient_scm.CreateSCM(url, self._root_dir, d)
606 scm.RunCommand(command, self._options, args, file_list)
607 self._options.revision = None
608 elif isinstance(deps[d], self.FileImpl):
maruel@chromium.org491c04b2010-05-17 18:17:44 +0000609 file_dep = deps[d]
610 self._options.revision = file_dep.GetRevision()
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000611 if run_scm:
maruel@chromium.org491c04b2010-05-17 18:17:44 +0000612 scm = gclient_scm.CreateSCM(file_dep.GetPath(), self._root_dir, d)
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000613 scm.RunCommand("updatesingle", self._options,
maruel@chromium.org491c04b2010-05-17 18:17:44 +0000614 args + [file_dep.GetFilename()], file_list)
maruel@chromium.org79692d62010-05-14 18:57:13 +0000615
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000616 if command == 'update' and not self._options.verbose:
617 pm.end()
piman@chromium.org6f363722010-04-27 00:41:09 +0000618
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000619 # Second pass for inherited deps (via the From keyword)
620 for d in deps_to_process:
621 if isinstance(deps[d], self.FromImpl):
622 filename = os.path.join(self._root_dir,
623 deps[d].module_name,
maruel@chromium.org1f7d1182010-05-17 18:17:38 +0000624 self.DEPS_FILE)
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000625 content = gclient_utils.FileRead(filename)
626 sub_deps = self._ParseSolutionDeps(deps[d].module_name, content, {},
627 False)
628 # Getting the URL from the sub_deps file can involve having to resolve
629 # a File() or having to resolve a relative URL. To resolve relative
630 # URLs, we need to pass in the orignal sub deps URL.
631 sub_deps_base_url = deps[deps[d].module_name]
632 url = deps[d].GetUrl(d, sub_deps_base_url, self._root_dir, sub_deps)
633 entries[d] = url
634 if run_scm:
635 self._options.revision = revision_overrides.get(d)
636 scm = gclient_scm.CreateSCM(url, self._root_dir, d)
637 scm.RunCommand(command, self._options, args, file_list)
638 self._options.revision = None
gspencer@google.comdf2d5902009-09-11 22:16:21 +0000639
phajdan.jr@chromium.orgd83b2b22009-08-11 15:30:55 +0000640 # Convert all absolute paths to relative.
641 for i in range(len(file_list)):
642 # TODO(phajdan.jr): We should know exactly when the paths are absolute.
643 # It depends on the command being executed (like runhooks vs sync).
644 if not os.path.isabs(file_list[i]):
645 continue
646
647 prefix = os.path.commonprefix([self._root_dir.lower(),
648 file_list[i].lower()])
649 file_list[i] = file_list[i][len(prefix):]
650
651 # Strip any leading path separators.
652 while file_list[i].startswith('\\') or file_list[i].startswith('/'):
653 file_list[i] = file_list[i][1:]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000654
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000655 is_using_git = gclient_utils.IsUsingGit(self._root_dir, entries.keys())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000656 self._RunHooks(command, file_list, is_using_git)
657
658 if command == 'update':
ajwong@chromium.orgcdcee802009-06-23 15:30:42 +0000659 # Notify the user if there is an orphaned entry in their working copy.
660 # Only delete the directory if there are no changes in it, and
661 # delete_unversioned_trees is set to true.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000662 prev_entries = self._ReadEntries()
663 for entry in prev_entries:
maruel@chromium.orgc5e9aec2009-08-03 18:25:56 +0000664 # Fix path separator on Windows.
665 entry_fixed = entry.replace('/', os.path.sep)
666 e_dir = os.path.join(self._root_dir, entry_fixed)
667 # Use entry and not entry_fixed there.
maruel@chromium.org0329e672009-05-13 18:41:04 +0000668 if entry not in entries and os.path.exists(e_dir):
msb@chromium.org83017012009-09-28 18:52:12 +0000669 modified_files = False
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000670 if isinstance(prev_entries, list):
msb@chromium.org83017012009-09-28 18:52:12 +0000671 # old .gclient_entries format was list, now dict
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000672 modified_files = gclient_scm.scm.SVN.CaptureStatus(e_dir)
msb@chromium.org83017012009-09-28 18:52:12 +0000673 else:
674 file_list = []
675 scm = gclient_scm.CreateSCM(prev_entries[entry], self._root_dir,
676 entry_fixed)
677 scm.status(self._options, [], file_list)
678 modified_files = file_list != []
679 if not self._options.delete_unversioned_trees or modified_files:
maruel@chromium.orgc5e9aec2009-08-03 18:25:56 +0000680 # There are modified files in this entry. Keep warning until
681 # removed.
maruel@chromium.orgc5e9aec2009-08-03 18:25:56 +0000682 print(("\nWARNING: \"%s\" is no longer part of this client. "
683 "It is recommended that you manually remove it.\n") %
684 entry_fixed)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000685 else:
686 # Delete the entry
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000687 print("\n________ deleting \'%s\' " +
maruel@chromium.orgc5e9aec2009-08-03 18:25:56 +0000688 "in \'%s\'") % (entry_fixed, self._root_dir)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000689 gclient_utils.RemoveDirectory(e_dir)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000690 # record the current list of entries for next time
691 self._SaveEntries(entries)
692
693 def PrintRevInfo(self):
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000694 """Output revision info mapping for the client and its dependencies.
695
696 This allows the capture of an overall "revision" for the source tree that
697 can be used to reproduce the same tree in the future. The actual output
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000698 contains enough information (source paths, svn server urls and revisions)
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000699 that it can be used either to generate external svn/git commands (without
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000700 gclient) or as input to gclient's --rev option (with some massaging of
701 the data).
702
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000703 Since we care about the revision of the current source tree, for git
704 repositories this command uses the revision of the HEAD. For subversion we
705 use BASE.
706
707 The --snapshot option allows creating a .gclient file to reproduce the tree.
708
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000709 Raises:
710 Error: If the client has conflicting entries.
711 """
712 # Check for revision overrides.
713 revision_overrides = {}
714 for revision in self._options.revisions:
715 if revision.find("@") < 0:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000716 raise gclient_utils.Error(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000717 "Specify the full dependency when specifying a revision number.")
718 revision_elem = revision.split("@")
719 # Disallow conflicting revs
720 if revision_overrides.has_key(revision_elem[0]) and \
721 revision_overrides[revision_elem[0]] != revision_elem[1]:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000722 raise gclient_utils.Error(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000723 "Conflicting revision numbers specified.")
724 revision_overrides[revision_elem[0]] = revision_elem[1]
725
726 solutions = self.GetVar("solutions")
727 if not solutions:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000728 raise gclient_utils.Error("No solution specified")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000729
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000730 # Inner helper to generate base url and rev tuple
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000731 def GetURLAndRev(name, original_url):
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000732 url, _ = gclient_utils.SplitUrlRevision(original_url)
733 scm = gclient_scm.CreateSCM(original_url, self._root_dir, name)
734 return (url, scm.revinfo(self._options, [], None))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000735
maruel@chromium.orge3da35f2010-03-09 21:40:45 +0000736 # text of the snapshot gclient file
737 new_gclient = ""
738 # Dictionary of { path : SCM url } to ensure no duplicate solutions
739 solution_names = {}
nasser@codeaurora.orgde8f3522010-03-11 23:47:44 +0000740 entries = {}
741 entries_deps_content = {}
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000742 # Run on the base solutions first.
743 for solution in solutions:
maruel@chromium.orge3da35f2010-03-09 21:40:45 +0000744 # Dictionary of { path : SCM url } to describe the gclient checkout
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000745 name = solution["name"]
maruel@chromium.orge3da35f2010-03-09 21:40:45 +0000746 if name in solution_names:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000747 raise gclient_utils.Error("solution %s specified more than once" % name)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000748 (url, rev) = GetURLAndRev(name, solution["url"])
msb@chromium.org770ff9e2009-09-23 17:18:18 +0000749 entries[name] = "%s@%s" % (url, rev)
maruel@chromium.orge3da35f2010-03-09 21:40:45 +0000750 solution_names[name] = "%s@%s" % (url, rev)
maruel@chromium.org491c04b2010-05-17 18:17:44 +0000751 deps_file = solution.get("deps_file", self.DEPS_FILE)
nasser@codeaurora.org952d7c72010-03-01 20:41:01 +0000752 if '/' in deps_file or '\\' in deps_file:
753 raise gclient_utils.Error('deps_file name must not be a path, just a '
754 'filename.')
755 try:
756 deps_content = gclient_utils.FileRead(
757 os.path.join(self._root_dir, name, deps_file))
758 except IOError, e:
759 if e.errno != errno.ENOENT:
760 raise
761 deps_content = ""
762 entries_deps_content[name] = deps_content
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000763
nasser@codeaurora.orgde8f3522010-03-11 23:47:44 +0000764 # Process the dependencies next (sort alphanumerically to ensure that
765 # containing directories get populated first and for readability)
766 deps = self._ParseAllDeps(entries, entries_deps_content)
767 deps_to_process = deps.keys()
768 deps_to_process.sort()
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000769
nasser@codeaurora.orgde8f3522010-03-11 23:47:44 +0000770 # First pass for direct dependencies.
771 for d in deps_to_process:
772 if type(deps[d]) == str:
773 (url, rev) = GetURLAndRev(d, deps[d])
774 entries[d] = "%s@%s" % (url, rev)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000775
nasser@codeaurora.orgde8f3522010-03-11 23:47:44 +0000776 # Second pass for inherited deps (via the From keyword)
777 for d in deps_to_process:
tony@chromium.org4b5b1772010-04-08 01:52:56 +0000778 if isinstance(deps[d], self.FromImpl):
nasser@codeaurora.orgde8f3522010-03-11 23:47:44 +0000779 deps_parent_url = entries[deps[d].module_name]
780 if deps_parent_url.find("@") < 0:
781 raise gclient_utils.Error("From %s missing revisioned url" %
782 deps[d].module_name)
783 content = gclient_utils.FileRead(os.path.join(
784 self._root_dir,
785 deps[d].module_name,
maruel@chromium.org491c04b2010-05-17 18:17:44 +0000786 self.DEPS_FILE))
787 sub_deps = self._ParseSolutionDeps(deps[d].module_name, content, {},
788 False)
nasser@codeaurora.orgde8f3522010-03-11 23:47:44 +0000789 (url, rev) = GetURLAndRev(d, sub_deps[d])
790 entries[d] = "%s@%s" % (url, rev)
maruel@chromium.orge3da35f2010-03-09 21:40:45 +0000791
nasser@codeaurora.orgde8f3522010-03-11 23:47:44 +0000792 # Build the snapshot configuration string
793 if self._options.snapshot:
794 url = entries.pop(name)
795 custom_deps = ",\n ".join(["\"%s\": \"%s\"" % (x, entries[x])
796 for x in sorted(entries.keys())])
maruel@chromium.orge3da35f2010-03-09 21:40:45 +0000797
maruel@chromium.org1f7d1182010-05-17 18:17:38 +0000798 new_gclient += self.DEFAULT_SNAPSHOT_SOLUTION_TEXT % {
nasser@codeaurora.orgde8f3522010-03-11 23:47:44 +0000799 'solution_name': name,
800 'solution_url': url,
801 'safesync_url' : "",
802 'solution_deps': custom_deps,
803 }
804 else:
805 print(";\n".join(["%s: %s" % (x, entries[x])
806 for x in sorted(entries.keys())]))
maruel@chromium.orge3da35f2010-03-09 21:40:45 +0000807
808 # Print the snapshot configuration file
809 if self._options.snapshot:
maruel@chromium.org491c04b2010-05-17 18:17:44 +0000810 config = self.DEFAULT_SNAPSHOT_FILE_TEXT % {'solution_list': new_gclient}
maruel@chromium.orge3da35f2010-03-09 21:40:45 +0000811 snapclient = GClient(self._root_dir, self._options)
812 snapclient.SetConfig(config)
813 print(snapclient._config_content)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000814
815
816## gclient commands.
817
818
maruel@chromium.orgddff62d2010-05-17 21:02:36 +0000819def CMDcleanup(parser, options, args):
maruel@chromium.org79692d62010-05-14 18:57:13 +0000820 """Clean up all working copies, using 'svn cleanup' for each module.
maruel@chromium.orgddff62d2010-05-17 21:02:36 +0000821
maruel@chromium.org79692d62010-05-14 18:57:13 +0000822Additional options and args may be passed to 'svn cleanup'.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000823
maruel@chromium.org79692d62010-05-14 18:57:13 +0000824usage: cleanup [options] [--] [svn cleanup args/options]
825
826Valid options:
827 --verbose : output additional diagnostics
828"""
maruel@chromium.org2806acc2009-05-15 12:33:34 +0000829 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000830 if not client:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000831 raise gclient_utils.Error("client not configured; see 'gclient config'")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000832 if options.verbose:
833 # Print out the .gclient file. This is longer than if we just printed the
834 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000835 print(client.ConfigContent())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000836 return client.RunOnDeps('cleanup', args)
837
838
maruel@chromium.orgddff62d2010-05-17 21:02:36 +0000839def CMDconfig(parser, options, args):
840 """Create a .gclient file in the current directory.
841
842This specifies the configuration for further commands. After update/sync,
maruel@chromium.org79692d62010-05-14 18:57:13 +0000843top-level DEPS files in each module are read to determine dependent
844modules to operate on as well. If optional [url] parameter is
845provided, then configuration is read from a specified Subversion server
846URL. Otherwise, a --spec option must be provided. A --name option overrides
847the default name for the solutions.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000848
maruel@chromium.org79692d62010-05-14 18:57:13 +0000849usage: config [option | url] [safesync url]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000850
maruel@chromium.org79692d62010-05-14 18:57:13 +0000851Valid options:
852 --name path : alternate relative path for the solution
853 --spec=GCLIENT_SPEC : contents of .gclient are read from string parameter.
854 *Note that due to Cygwin/Python brokenness, it
855 probably can't contain any newlines.*
856
857Examples:
858 gclient config https://gclient.googlecode.com/svn/trunk/gclient
859 configure a new client to check out gclient.py tool sources
860 gclient config --name tools https://gclient.googlecode.com/svn/trunk/gclient
861 gclient config --spec='solutions=[{"name":"gclient",
862 '"url":"https://gclient.googlecode.com/svn/trunk/gclient",'
863 '"custom_deps":{}}]'
864"""
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000865 if len(args) < 1 and not options.spec:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000866 raise gclient_utils.Error("required argument missing; see 'gclient help "
867 "config'")
maruel@chromium.org0329e672009-05-13 18:41:04 +0000868 if os.path.exists(options.config_filename):
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000869 raise gclient_utils.Error("%s file already exists in the current directory"
870 % options.config_filename)
maruel@chromium.org2806acc2009-05-15 12:33:34 +0000871 client = GClient('.', options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000872 if options.spec:
873 client.SetConfig(options.spec)
874 else:
maruel@chromium.org1ab7ffc2009-06-03 17:21:37 +0000875 base_url = args[0].rstrip('/')
iposva@chromium.org8cf7a392010-04-07 17:20:26 +0000876 if not options.name:
877 name = base_url.split("/")[-1]
878 else:
879 # specify an alternate relpath for the given URL.
880 name = options.name
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000881 safesync_url = ""
882 if len(args) > 1:
883 safesync_url = args[1]
884 client.SetDefaultConfig(name, base_url, safesync_url)
885 client.SaveConfig()
maruel@chromium.org79692d62010-05-14 18:57:13 +0000886 return 0
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000887
888
maruel@chromium.orgddff62d2010-05-17 21:02:36 +0000889def CMDexport(parser, options, args):
890 """Wrapper for svn export for all managed directories."""
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +0000891 if len(args) != 1:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000892 raise gclient_utils.Error("Need directory name")
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +0000893 client = GClient.LoadCurrentConfig(options)
894
895 if not client:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000896 raise gclient_utils.Error("client not configured; see 'gclient config'")
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +0000897
898 if options.verbose:
899 # Print out the .gclient file. This is longer than if we just printed the
900 # client dict, but more legible, and it might contain helpful comments.
901 print(client.ConfigContent())
902 return client.RunOnDeps('export', args)
903
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000904
maruel@chromium.orgddff62d2010-05-17 21:02:36 +0000905def CMDpack(parser, options, args):
maruel@chromium.org79692d62010-05-14 18:57:13 +0000906 """Generate a patch which can be applied at the root of the tree.
maruel@chromium.orgddff62d2010-05-17 21:02:36 +0000907
maruel@chromium.org79692d62010-05-14 18:57:13 +0000908Internally, runs 'svn diff' on each checked out module and
909dependencies, and performs minimal postprocessing of the output. The
910resulting patch is printed to stdout and can be applied to a freshly
911checked out tree via 'patch -p0 < patchfile'. Additional args and
912options to 'svn diff' can be passed after gclient options.
kbr@google.comab318592009-09-04 00:54:55 +0000913
maruel@chromium.org79692d62010-05-14 18:57:13 +0000914usage: pack [options] [--] [svn args/options]
915
916Valid options:
917 --verbose : output additional diagnostics
918
919Examples:
920 gclient pack > patch.txt
921 generate simple patch for configured client and dependences
922 gclient pack -- -x -b > patch.txt
923 generate patch using 'svn diff -x -b' to suppress
924 whitespace-only differences
925 gclient pack -- -r HEAD -x -b > patch.txt
926 generate patch, diffing each file versus the latest version of
927 each module
928"""
kbr@google.comab318592009-09-04 00:54:55 +0000929 client = GClient.LoadCurrentConfig(options)
930 if not client:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000931 raise gclient_utils.Error("client not configured; see 'gclient config'")
kbr@google.comab318592009-09-04 00:54:55 +0000932 if options.verbose:
933 # Print out the .gclient file. This is longer than if we just printed the
934 # client dict, but more legible, and it might contain helpful comments.
935 print(client.ConfigContent())
kbr@google.comab318592009-09-04 00:54:55 +0000936 return client.RunOnDeps('pack', args)
937
938
maruel@chromium.orgddff62d2010-05-17 21:02:36 +0000939def CMDstatus(parser, options, args):
940 """Show the modification status of for every dependencies.
941
942Additional options and args may be passed to 'svn status'.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000943
maruel@chromium.org79692d62010-05-14 18:57:13 +0000944usage: status [options] [--] [svn diff args/options]
945
946Valid options:
947 --verbose : output additional diagnostics
948 --nohooks : don't run the hooks after the update is complete
949"""
maruel@chromium.org2806acc2009-05-15 12:33:34 +0000950 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000951 if not client:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000952 raise gclient_utils.Error("client not configured; see 'gclient config'")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000953 if options.verbose:
954 # Print out the .gclient file. This is longer than if we just printed the
955 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000956 print(client.ConfigContent())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000957 return client.RunOnDeps('status', args)
958
959
maruel@chromium.orgddff62d2010-05-17 21:02:36 +0000960def CMDsync(parser, options, args):
961 """Checkout/update the modules specified by the gclient configuration.
962
963Unless --revision is specified, then the latest revision of the root solutions
964is checked out, with dependent submodule versions updated according to DEPS
965files. If --revision is specified, then the given revision is used in place
maruel@chromium.org79692d62010-05-14 18:57:13 +0000966of the latest, either for a single solution or for all solutions.
967Unless the --force option is provided, solutions and modules whose
968local revision matches the one to update (i.e., they have not changed
969in the repository) are *not* modified. Unless --nohooks is provided,
maruel@chromium.orgddff62d2010-05-17 21:02:36 +0000970the hooks are run. See 'help config' for more information.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000971
maruel@chromium.org79692d62010-05-14 18:57:13 +0000972usage: gclient sync [options] [--] [SCM update options/args]
973
974Valid options:
975 --force : force update even for unchanged modules
976 --nohooks : don't run the hooks after the update is complete
977 --revision SOLUTION@REV : update given solution to specified revision
978 --deps PLATFORM(S) : sync deps for the given platform(s), or 'all'
979 --verbose : output additional diagnostics
980 --head : update to latest revision, instead of last good
981 revision
982 --reset : resets any local changes before updating (git only)
983
984Examples:
985 gclient sync
986 update files from SCM according to current configuration,
987 *for modules which have changed since last update or sync*
988 gclient sync --force
989 update files from SCM according to current configuration, for
990 all modules (useful for recovering files deleted from local copy)
991 gclient sync --revision src@31000
992 update src directory to r31000
993"""
maruel@chromium.org2806acc2009-05-15 12:33:34 +0000994 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000995
996 if not client:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000997 raise gclient_utils.Error("client not configured; see 'gclient config'")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000998
999 if not options.head:
1000 solutions = client.GetVar('solutions')
1001 if solutions:
1002 for s in solutions:
1003 if s.get('safesync_url', ''):
1004 # rip through revisions and make sure we're not over-riding
1005 # something that was explicitly passed
1006 has_key = False
1007 for r in options.revisions:
1008 if r.split('@')[0] == s['name']:
1009 has_key = True
1010 break
1011
1012 if not has_key:
1013 handle = urllib.urlopen(s['safesync_url'])
1014 rev = handle.read().strip()
1015 handle.close()
1016 if len(rev):
1017 options.revisions.append(s['name']+'@'+rev)
1018
1019 if options.verbose:
1020 # Print out the .gclient file. This is longer than if we just printed the
1021 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +00001022 print(client.ConfigContent())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001023 return client.RunOnDeps('update', args)
1024
1025
maruel@chromium.orgddff62d2010-05-17 21:02:36 +00001026def CMDupdate(parser, options, args):
1027 """Alias for the sync command. Deprecated."""
1028 return CMDsync(parser, options, args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001029
maruel@chromium.org79692d62010-05-14 18:57:13 +00001030
maruel@chromium.orgddff62d2010-05-17 21:02:36 +00001031def CMDdiff(parser, options, args):
maruel@chromium.org79692d62010-05-14 18:57:13 +00001032 """Display the differences between two revisions of modules.
maruel@chromium.orgddff62d2010-05-17 21:02:36 +00001033
maruel@chromium.org79692d62010-05-14 18:57:13 +00001034(Does 'svn diff' for each checked out module and dependences.)
1035Additional args and options to 'svn diff' can be passed after
1036gclient options.
1037
1038usage: diff [options] [--] [svn args/options]
1039
1040Valid options:
1041 --verbose : output additional diagnostics
1042
1043Examples:
1044 gclient diff
1045 simple 'svn diff' for configured client and dependences
1046 gclient diff -- -x -b
1047 use 'svn diff -x -b' to suppress whitespace-only differences
1048 gclient diff -- -r HEAD -x -b
1049 diff versus the latest version of each module
1050"""
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001051 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001052 if not client:
maruel@chromium.orge3608df2009-11-10 20:22:57 +00001053 raise gclient_utils.Error("client not configured; see 'gclient config'")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001054 if options.verbose:
1055 # Print out the .gclient file. This is longer than if we just printed the
1056 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +00001057 print(client.ConfigContent())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001058 return client.RunOnDeps('diff', args)
1059
1060
maruel@chromium.orgddff62d2010-05-17 21:02:36 +00001061def CMDrevert(parser, options, args):
1062 """Revert every file in every managed directory in the client view."""
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001063 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001064 if not client:
maruel@chromium.orge3608df2009-11-10 20:22:57 +00001065 raise gclient_utils.Error("client not configured; see 'gclient config'")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001066 return client.RunOnDeps('revert', args)
1067
1068
maruel@chromium.orgddff62d2010-05-17 21:02:36 +00001069def CMDrunhooks(parser, options, args):
1070 """Runs hooks for files that have been modified in the local working copy.
1071
1072Implies --force.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001073
maruel@chromium.org79692d62010-05-14 18:57:13 +00001074usage: runhooks [options]
1075
1076Valid options:
1077 --verbose : output additional diagnostics
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:
maruel@chromium.orge3608df2009-11-10 20:22:57 +00001081 raise gclient_utils.Error("client not configured; see 'gclient config'")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001082 if options.verbose:
1083 # Print out the .gclient file. This is longer than if we just printed the
1084 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +00001085 print(client.ConfigContent())
maruel@chromium.org5df6a462009-08-28 18:52:26 +00001086 options.force = True
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001087 return client.RunOnDeps('runhooks', args)
1088
1089
maruel@chromium.orgddff62d2010-05-17 21:02:36 +00001090def CMDrevinfo(parser, options, args):
1091 """Outputs defails for every dependencies.
1092
1093This includes source path, server URL and revision information for every
maruel@chromium.org79692d62010-05-14 18:57:13 +00001094dependency in all solutions.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001095
maruel@chromium.org79692d62010-05-14 18:57:13 +00001096usage: revinfo [options]
1097"""
maruel@chromium.orge3608df2009-11-10 20:22:57 +00001098 __pychecker__ = 'unusednames=args'
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001099 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001100 if not client:
maruel@chromium.orge3608df2009-11-10 20:22:57 +00001101 raise gclient_utils.Error("client not configured; see 'gclient config'")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001102 client.PrintRevInfo()
maruel@chromium.org79692d62010-05-14 18:57:13 +00001103 return 0
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001104
1105
maruel@chromium.orgddff62d2010-05-17 21:02:36 +00001106def CMDhelp(parser, options, args):
1107 """Prints general help or command-specific documentation."""
1108 if len(args) == 1:
1109 command = Command(args[0])
1110 if command:
1111 print getattr(sys.modules[__name__], 'CMD' + args[0]).__doc__
1112 return 0
1113 parser.usage = (DEFAULT_USAGE_TEXT + '\nCommands are:\n' + '\n'.join([
1114 ' %-10s %s' % (fn[3:], Command(fn[3:]).__doc__.split('\n')[0].strip())
1115 for fn in dir(sys.modules[__name__]) if fn.startswith('CMD')]))
1116 parser.print_help()
1117 return 0
1118
1119
1120def Command(command):
1121 return getattr(sys.modules[__name__], 'CMD' + command, CMDhelp)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001122
1123
1124def Main(argv):
maruel@chromium.orgddff62d2010-05-17 21:02:36 +00001125 parser = optparse.OptionParser(usage=DEFAULT_USAGE_TEXT,
1126 version='%prog ' + __version__)
1127 parser.add_option("-v", "--verbose", action="count", default=0,
1128 help="Produces additional output for diagnostics. Can be "
1129 "used up to three times for more logging info.")
1130 parser.add_option("--gclientfile", metavar="FILENAME", dest="config_filename",
1131 default=os.environ.get("GCLIENT_FILE", ".gclient"),
1132 help="Specify an alternate .gclient file")
1133 # The other options will be moved eventually.
1134 parser.add_option("--force", action="store_true",
1135 help="(update/sync only) force update even "
1136 "for modules which haven't changed")
1137 parser.add_option("--nohooks", action="store_true",
1138 help="(update/sync/revert only) prevent the hooks "
1139 "from running")
1140 parser.add_option("--revision", action="append", dest="revisions",
1141 metavar="REV", default=[],
1142 help="(update/sync only) sync to a specific "
1143 "revision, can be used multiple times for "
1144 "each solution, e.g. --revision=src@123, "
1145 "--revision=internal@32")
1146 parser.add_option("--deps", dest="deps_os", metavar="OS_LIST",
1147 help="(update/sync only) sync deps for the "
1148 "specified (comma-separated) platform(s); "
1149 "'all' will sync all platforms")
1150 parser.add_option("--reset", action="store_true",
1151 help="(update/sync only) resets any local changes "
1152 "before updating (git only)")
1153 parser.add_option("--spec",
1154 help="(config only) create a gclient file "
1155 "containing the provided string")
1156 parser.add_option("--manually_grab_svn_rev", action="store_true",
1157 help="Skip svn up whenever possible by requesting "
1158 "actual HEAD revision from the repository")
1159 parser.add_option("--head", action="store_true",
1160 help="skips any safesync_urls specified in "
1161 "configured solutions")
1162 parser.add_option("--delete_unversioned_trees", action="store_true",
1163 help="on update, delete any unexpected "
1164 "unversioned trees that are in the checkout")
1165 parser.add_option("--snapshot", action="store_true",
1166 help="(revinfo only), create a snapshot file "
1167 "of the current version of all repositories")
1168 parser.add_option("--name",
1169 help="specify alternate relative solution path")
1170 # Integrate standard options processing.
1171 old_parser = parser.parse_args
1172 def Parse(args):
1173 (options, args) = old_parser(args)
1174 if options.verbose == 2:
1175 logging.basicConfig(level=logging.INFO)
1176 elif options.verbose > 2:
1177 logging.basicConfig(level=logging.DEBUG)
1178 options.entries_filename = options.config_filename + "_entries"
1179 return (options, args)
1180 parser.parse_args = Parse
1181 # We don't want wordwrapping in epilog (usually examples)
1182 parser.format_epilog = lambda _: parser.epilog or ''
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001183
maruel@chromium.orgddff62d2010-05-17 21:02:36 +00001184 if not len(argv):
1185 argv = ['help']
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001186 # Add manual support for --version as first argument.
maruel@chromium.orgddff62d2010-05-17 21:02:36 +00001187 if argv[0] == '--version':
1188 parser.print_version()
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001189 return 0
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001190 # Add manual support for --help as first argument.
maruel@chromium.orgddff62d2010-05-17 21:02:36 +00001191 if argv[0] == '--help':
1192 argv[0] = 'help'
1193 options, args = parser.parse_args(argv[1:])
1194 return Command(argv[0])(parser, options, args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001195
1196
1197if "__main__" == __name__:
1198 try:
maruel@chromium.orgddff62d2010-05-17 21:02:36 +00001199 sys.exit(Main(sys.argv[1:]))
maruel@chromium.orge3608df2009-11-10 20:22:57 +00001200 except gclient_utils.Error, e:
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +00001201 print >> sys.stderr, "Error: %s" % str(e)
maruel@chromium.orgddff62d2010-05-17 21:02:36 +00001202 sys.exit(1)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001203
1204# vim: ts=2:sw=2:tw=80:et: