blob: 1b7da5cdc82f0edae49e61b679c379acf7f2234a [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
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00006"""Meta checkout manager supporting both Subversion and GIT.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00007
8Files
9 .gclient : Current client configuration, written by 'config' command.
10 Format is a Python script defining 'solutions', a list whose
11 entries each are maps binding the strings "name" and "url"
12 to strings specifying the name and location of the client
13 module, as well as "custom_deps" to a map similar to the DEPS
14 file below.
15 .gclient_entries : A cache constructed by 'update' command. Format is a
16 Python script defining 'entries', a list of the names
17 of all modules in the client
18 <module>/DEPS : Python script defining var 'deps' as a map from each requisite
19 submodule name to a URL where it can be found (via one SCM)
20
21Hooks
22 .gclient and DEPS files may optionally contain a list named "hooks" to
23 allow custom actions to be performed based on files that have changed in the
evan@chromium.org67820ef2009-07-27 17:23:00 +000024 working copy as a result of a "sync"/"update" or "revert" operation. This
maruel@chromium.org0b6a0842010-06-15 14:34:19 +000025 can be prevented by using --nohooks (hooks run by default). Hooks can also
maruel@chromium.org5df6a462009-08-28 18:52:26 +000026 be forced to run with the "runhooks" operation. If "sync" is run with
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000027 --force, all known hooks will run regardless of the state of the working
28 copy.
29
30 Each item in a "hooks" list is a dict, containing these two keys:
31 "pattern" The associated value is a string containing a regular
32 expression. When a file whose pathname matches the expression
33 is checked out, updated, or reverted, the hook's "action" will
34 run.
35 "action" A list describing a command to run along with its arguments, if
36 any. An action command will run at most one time per gclient
37 invocation, regardless of how many files matched the pattern.
38 The action is executed in the same directory as the .gclient
39 file. If the first item in the list is the string "python",
40 the current Python interpreter (sys.executable) will be used
phajdan.jr@chromium.org71b40682009-07-31 23:40:09 +000041 to run the command. If the list contains string "$matching_files"
42 it will be removed from the list and the list will be extended
43 by the list of matching files.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000044
45 Example:
46 hooks = [
47 { "pattern": "\\.(gif|jpe?g|pr0n|png)$",
48 "action": ["python", "image_indexer.py", "--all"]},
49 ]
50"""
51
maruel@chromium.org0b6a0842010-06-15 14:34:19 +000052__version__ = "0.4.1"
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000053
54import errno
maruel@chromium.org754960e2009-09-21 12:31:05 +000055import logging
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000056import optparse
57import os
msb@chromium.org2e38de72009-09-28 17:04:47 +000058import pprint
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000059import re
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000060import sys
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000061import urlparse
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000062import urllib
63
maruel@chromium.orgada4c652009-12-03 15:32:01 +000064import breakpad
65
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000066import gclient_scm
67import gclient_utils
nasser@codeaurora.org1f7a3d12010-02-04 15:11:50 +000068from third_party.repo.progress import Progress
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000069
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000070
maruel@chromium.org1f7d1182010-05-17 18:17:38 +000071def attr(attr, data):
72 """Sets an attribute on a function."""
73 def hook(fn):
74 setattr(fn, attr, data)
75 return fn
76 return hook
maruel@chromium.orge3da35f2010-03-09 21:40:45 +000077
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000078
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000079## GClient implementation.
80
81
maruel@chromium.org116704f2010-06-11 17:34:38 +000082class GClientKeywords(object):
83 class FromImpl(object):
84 """Used to implement the From() syntax."""
85
86 def __init__(self, module_name, sub_target_name=None):
87 """module_name is the dep module we want to include from. It can also be
88 the name of a subdirectory to include from.
89
90 sub_target_name is an optional parameter if the module name in the other
91 DEPS file is different. E.g., you might want to map src/net to net."""
92 self.module_name = module_name
93 self.sub_target_name = sub_target_name
94
95 def __str__(self):
96 return 'From(%s, %s)' % (repr(self.module_name),
97 repr(self.sub_target_name))
98
99 def GetUrl(self, target_name, sub_deps_base_url, root_dir, sub_deps):
100 """Resolve the URL for this From entry."""
101 sub_deps_target_name = target_name
102 if self.sub_target_name:
103 sub_deps_target_name = self.sub_target_name
104 url = sub_deps[sub_deps_target_name]
105 if url.startswith('/'):
106 # If it's a relative URL, we need to resolve the URL relative to the
107 # sub deps base URL.
108 if not isinstance(sub_deps_base_url, basestring):
109 sub_deps_base_url = sub_deps_base_url.GetPath()
110 scm = gclient_scm.CreateSCM(sub_deps_base_url, root_dir,
111 None)
112 url = scm.FullUrlForRelativeUrl(url)
113 return url
114
115 class FileImpl(object):
116 """Used to implement the File('') syntax which lets you sync a single file
117 from an SVN repo."""
118
119 def __init__(self, file_location):
120 self.file_location = file_location
121
122 def __str__(self):
123 return 'File("%s")' % self.file_location
124
125 def GetPath(self):
126 return os.path.split(self.file_location)[0]
127
128 def GetFilename(self):
129 rev_tokens = self.file_location.split('@')
130 return os.path.split(rev_tokens[0])[1]
131
132 def GetRevision(self):
133 rev_tokens = self.file_location.split('@')
134 if len(rev_tokens) > 1:
135 return rev_tokens[1]
136 return None
137
138 class VarImpl(object):
139 def __init__(self, custom_vars, local_scope):
140 self._custom_vars = custom_vars
141 self._local_scope = local_scope
142
143 def Lookup(self, var_name):
144 """Implements the Var syntax."""
145 if var_name in self._custom_vars:
146 return self._custom_vars[var_name]
147 elif var_name in self._local_scope.get("vars", {}):
148 return self._local_scope["vars"][var_name]
149 raise gclient_utils.Error("Var is not defined: %s" % var_name)
150
151
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000152class Dependency(GClientKeywords):
153 """Object that represents a dependency checkout."""
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000154 DEPS_FILE = 'DEPS'
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000155
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000156 def __init__(self, parent, name, url, safesync_url=None, custom_deps=None,
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000157 custom_vars=None, deps_file=None):
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000158 GClientKeywords.__init__(self)
159 self.parent = parent
160 self.name = name
161 self.url = url
162 # These 2 are only set in .gclient and not in DEPS files.
163 self.safesync_url = safesync_url
164 self.custom_vars = custom_vars or {}
165 self.custom_deps = custom_deps or {}
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000166 self.deps_hooks = []
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000167 self.dependencies = []
168 self.deps_file = deps_file or self.DEPS_FILE
maruel@chromium.org271375b2010-06-23 19:17:38 +0000169 self.deps_parsed = False
170 self.direct_reference = False
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000171
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000172 # Sanity checks
173 if not self.name and self.parent:
174 raise gclient_utils.Error('Dependency without name')
175 if not isinstance(self.url,
176 (basestring, self.FromImpl, self.FileImpl, None.__class__)):
177 raise gclient_utils.Error('dependency url must be either a string, None, '
178 'File() or From() instead of %s' %
179 self.url.__class__.__name__)
180 if '/' in self.deps_file or '\\' in self.deps_file:
181 raise gclient_utils.Error('deps_file name must not be a path, just a '
182 'filename. %s' % self.deps_file)
183
maruel@chromium.org271375b2010-06-23 19:17:38 +0000184 def ParseDepsFile(self, direct_reference):
185 """Parses the DEPS file for this dependency."""
186 if direct_reference:
187 # Maybe it was referenced earlier by a From() keyword but it's now
188 # directly referenced.
189 self.direct_reference = direct_reference
190 self.deps_parsed = True
191 filepath = os.path.join(self.root_dir(), self.name, self.deps_file)
192 if not os.path.isfile(filepath):
maruel@chromium.org0d425922010-06-21 19:22:24 +0000193 return {}
maruel@chromium.org271375b2010-06-23 19:17:38 +0000194 deps_content = gclient_utils.FileRead(filepath)
maruel@chromium.org0d425922010-06-21 19:22:24 +0000195
maruel@chromium.org271375b2010-06-23 19:17:38 +0000196 # Eval the content.
197 # One thing is unintuitive, vars= {} must happen before Var() use.
198 local_scope = {}
199 var = self.VarImpl(self.custom_vars, local_scope)
200 global_scope = {
201 'File': self.FileImpl,
202 'From': self.FromImpl,
203 'Var': var.Lookup,
204 'deps_os': {},
205 }
206 exec(deps_content, global_scope, local_scope)
207 deps = local_scope.get('deps', {})
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000208 # load os specific dependencies if defined. these dependencies may
209 # override or extend the values defined by the 'deps' member.
maruel@chromium.org271375b2010-06-23 19:17:38 +0000210 if 'deps_os' in local_scope:
211 for deps_os_key in self.enforced_os():
212 os_deps = local_scope['deps_os'].get(deps_os_key, {})
213 if len(self.enforced_os()) > 1:
214 # Ignore any conflict when including deps for more than one
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000215 # platform, so we collect the broadest set of dependencies available.
216 # We may end up with the wrong revision of something for our
217 # platform, but this is the best we can do.
218 deps.update([x for x in os_deps.items() if not x[0] in deps])
219 else:
220 deps.update(os_deps)
221
maruel@chromium.org271375b2010-06-23 19:17:38 +0000222 self.deps_hooks.extend(local_scope.get('hooks', []))
223
224 # If a line is in custom_deps, but not in the solution, we want to append
225 # this line to the solution.
226 for d in self.custom_deps:
227 if d not in deps:
228 deps[d] = self.custom_deps[d]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000229
230 # If use_relative_paths is set in the DEPS file, regenerate
231 # the dictionary using paths relative to the directory containing
232 # the DEPS file.
maruel@chromium.org271375b2010-06-23 19:17:38 +0000233 use_relative_paths = local_scope.get('use_relative_paths', False)
234 if use_relative_paths:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000235 rel_deps = {}
236 for d, url in deps.items():
237 # normpath is required to allow DEPS to use .. in their
238 # dependency local path.
maruel@chromium.org271375b2010-06-23 19:17:38 +0000239 rel_deps[os.path.normpath(os.path.join(self.name, d))] = url
240 deps = rel_deps
241 # TODO(maruel): Add these dependencies into self.dependencies.
242 return deps
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000243
maruel@chromium.org271375b2010-06-23 19:17:38 +0000244 def _ParseAllDeps(self, solution_urls):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000245 """Parse the complete list of dependencies for the client.
246
247 Args:
248 solution_urls: A dict mapping module names (as relative paths) to URLs
249 corresponding to the solutions specified by the client. This parameter
250 is passed as an optimization.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000251
252 Returns:
253 A dict mapping module names (as relative paths) to URLs corresponding
254 to the entire set of dependencies to checkout for the given client.
255
256 Raises:
257 Error: If a dependency conflicts with another dependency or of a solution.
258 """
259 deps = {}
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000260 for solution in self.dependencies:
maruel@chromium.org271375b2010-06-23 19:17:38 +0000261 solution_deps = solution.ParseDepsFile(True)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000262
263 # If a line is in custom_deps, but not in the solution, we want to append
264 # this line to the solution.
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000265 for d in solution.custom_deps:
266 if d not in solution_deps:
267 solution_deps[d] = solution.custom_deps[d]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000268
269 for d in solution_deps:
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000270 if d in solution.custom_deps:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000271 # Dependency is overriden.
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000272 url = solution.custom_deps[d]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000273 if url is None:
274 continue
275 else:
276 url = solution_deps[d]
277 # if we have a From reference dependent on another solution, then
278 # just skip the From reference. When we pull deps for the solution,
279 # we will take care of this dependency.
280 #
281 # If multiple solutions all have the same From reference, then we
282 # should only add one to our list of dependencies.
tony@chromium.org4b5b1772010-04-08 01:52:56 +0000283 if isinstance(url, self.FromImpl):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000284 if url.module_name in solution_urls:
285 # Already parsed.
286 continue
287 if d in deps and type(deps[d]) != str:
288 if url.module_name == deps[d].module_name:
289 continue
tony@chromium.org4b5b1772010-04-08 01:52:56 +0000290 elif isinstance(url, str):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000291 parsed_url = urlparse.urlparse(url)
292 scheme = parsed_url[0]
293 if not scheme:
294 # A relative url. Fetch the real base.
295 path = parsed_url[2]
296 if path[0] != "/":
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000297 raise gclient_utils.Error(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000298 "relative DEPS entry \"%s\" must begin with a slash" % d)
msb@chromium.orge6f78352010-01-13 17:05:33 +0000299 # Create a scm just to query the full url.
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000300 scm = gclient_scm.CreateSCM(solution.url, self.root_dir(),
301 None)
msb@chromium.orge6f78352010-01-13 17:05:33 +0000302 url = scm.FullUrlForRelativeUrl(url)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000303 if d in deps and deps[d] != url:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000304 raise gclient_utils.Error(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000305 "Solutions have conflicting versions of dependency \"%s\"" % d)
306 if d in solution_urls and solution_urls[d] != url:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000307 raise gclient_utils.Error(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000308 "Dependency \"%s\" conflicts with specified solution" % d)
309 # Grab the dependency.
310 deps[d] = url
311 return deps
312
phajdan.jr@chromium.org71b40682009-07-31 23:40:09 +0000313 def _RunHookAction(self, hook_dict, matching_file_list):
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000314 """Runs the action from a single hook."""
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000315 logging.info(hook_dict)
316 logging.info(matching_file_list)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000317 command = hook_dict['action'][:]
318 if command[0] == 'python':
319 # If the hook specified "python" as the first item, the action is a
320 # Python script. Run it by starting a new copy of the same
321 # interpreter.
322 command[0] = sys.executable
323
phajdan.jr@chromium.org71b40682009-07-31 23:40:09 +0000324 if '$matching_files' in command:
phajdan.jr@chromium.org68f2e092009-08-06 17:05:35 +0000325 splice_index = command.index('$matching_files')
326 command[splice_index:splice_index + 1] = matching_file_list
phajdan.jr@chromium.org71b40682009-07-31 23:40:09 +0000327
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000328 # Use a discrete exit status code of 2 to indicate that a hook action
329 # failed. Users of this script may wish to treat hook action failures
330 # differently from VC failures.
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000331 return gclient_utils.SubprocessCall(command, self.root_dir(), fail_status=2)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000332
333 def _RunHooks(self, command, file_list, is_using_git):
334 """Evaluates all hooks, running actions as needed.
335 """
336 # Hooks only run for these command types.
337 if not command in ('update', 'revert', 'runhooks'):
338 return
339
evan@chromium.org67820ef2009-07-27 17:23:00 +0000340 # Hooks only run when --nohooks is not specified
341 if self._options.nohooks:
342 return
343
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000344 # Get any hooks from the .gclient file.
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000345 hooks = self.deps_hooks[:]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000346 # Add any hooks found in DEPS files.
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000347 for d in self.dependencies:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000348 hooks.extend(d.deps_hooks)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000349
350 # If "--force" was specified, run all hooks regardless of what files have
351 # changed. If the user is using git, then we don't know what files have
352 # changed so we always run all hooks.
353 if self._options.force or is_using_git:
354 for hook_dict in hooks:
phajdan.jr@chromium.org71b40682009-07-31 23:40:09 +0000355 self._RunHookAction(hook_dict, [])
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000356 return
357
358 # Run hooks on the basis of whether the files from the gclient operation
359 # match each hook's pattern.
360 for hook_dict in hooks:
361 pattern = re.compile(hook_dict['pattern'])
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000362 matching_file_list = [f for f in file_list if pattern.search(f)]
phajdan.jr@chromium.org71b40682009-07-31 23:40:09 +0000363 if matching_file_list:
364 self._RunHookAction(hook_dict, matching_file_list)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000365
maruel@chromium.org271375b2010-06-23 19:17:38 +0000366 def root_dir(self):
367 return self.parent.root_dir()
368
369 def enforced_os(self):
370 return self.parent.enforced_os()
371
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000372 def recursion_limit(self):
373 return self.parent.recursion_limit() - 1
374
375 def tree(self, force_all):
376 return self.parent.tree(force_all)
377
378 def get_custom_deps(self, name, url):
379 """Returns a custom deps if applicable."""
380 if self.parent:
381 url = self.parent.get_custom_deps(name, url)
382 # None is a valid return value to disable a dependency.
383 return self.custom_deps.get(name, url)
384
385 def __str__(self):
386 out = []
387 for i in ('name', 'url', 'safesync_url', 'custom_deps', 'custom_vars',
388 'deps_hooks'):
389 # 'deps_file'
390 if self.__dict__[i]:
391 out.append('%s: %s' % (i, self.__dict__[i]))
392
393 for d in self.dependencies:
394 out.extend([' ' + x for x in str(d).splitlines()])
395 out.append('')
396 return '\n'.join(out)
397
398 def __repr__(self):
399 return '%s: %s' % (self.name, self.url)
400
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000401
402class GClient(Dependency):
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000403 """Object that represent a gclient checkout. A tree of Dependency(), one per
404 solution or DEPS entry."""
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000405 SUPPORTED_COMMANDS = [
406 'cleanup', 'diff', 'export', 'pack', 'revert', 'status', 'update',
407 'runhooks'
408 ]
409
410 DEPS_OS_CHOICES = {
411 "win32": "win",
412 "win": "win",
413 "cygwin": "win",
414 "darwin": "mac",
415 "mac": "mac",
416 "unix": "unix",
417 "linux": "unix",
418 "linux2": "unix",
419 }
420
421 DEFAULT_CLIENT_FILE_TEXT = ("""\
422solutions = [
423 { "name" : "%(solution_name)s",
424 "url" : "%(solution_url)s",
425 "custom_deps" : {
426 },
427 "safesync_url": "%(safesync_url)s"
428 },
429]
430""")
431
432 DEFAULT_SNAPSHOT_SOLUTION_TEXT = ("""\
433 { "name" : "%(solution_name)s",
434 "url" : "%(solution_url)s",
435 "custom_deps" : {
436 %(solution_deps)s,
437 },
438 "safesync_url": "%(safesync_url)s"
439 },
440""")
441
442 DEFAULT_SNAPSHOT_FILE_TEXT = ("""\
443# Snapshot generated with gclient revinfo --snapshot
444solutions = [
445%(solution_list)s
446]
447""")
448
449 def __init__(self, root_dir, options):
450 Dependency.__init__(self, None, None, None)
maruel@chromium.org0d425922010-06-21 19:22:24 +0000451 self._options = options
maruel@chromium.org271375b2010-06-23 19:17:38 +0000452 if options.deps_os:
453 enforced_os = options.deps_os.split(',')
454 else:
455 enforced_os = [self.DEPS_OS_CHOICES.get(sys.platform, 'unix')]
456 if 'all' in enforced_os:
457 enforced_os = self.DEPS_OS_CHOICES.itervalues()
458 self._enforced_os = list(set(enforced_os))
459 self._root_dir = root_dir
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000460 self.config_content = None
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000461 # Do not change previous behavior. Only solution level and immediate DEPS
462 # are processed.
463 self._recursion_limit = 2
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000464
465 def SetConfig(self, content):
466 assert self.dependencies == []
467 config_dict = {}
468 self.config_content = content
469 try:
470 exec(content, config_dict)
471 except SyntaxError, e:
472 try:
473 # Try to construct a human readable error message
474 error_message = [
475 'There is a syntax error in your configuration file.',
476 'Line #%s, character %s:' % (e.lineno, e.offset),
477 '"%s"' % re.sub(r'[\r\n]*$', '', e.text) ]
478 except:
479 # Something went wrong, re-raise the original exception
480 raise e
481 else:
482 # Raise a new exception with the human readable message:
483 raise gclient_utils.Error('\n'.join(error_message))
484 for s in config_dict.get('solutions', []):
maruel@chromium.org81843b82010-06-28 16:49:26 +0000485 try:
486 self.dependencies.append(Dependency(
487 self, s['name'], s['url'],
488 s.get('safesync_url', None),
489 s.get('custom_deps', {}),
490 s.get('custom_vars', {})))
491 except KeyError:
492 raise gclient_utils.Error('Invalid .gclient file. Solution is '
493 'incomplete: %s' % s)
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000494 # .gclient can have hooks.
495 self.deps_hooks = config_dict.get('hooks', [])
496
497 def SaveConfig(self):
498 gclient_utils.FileWrite(os.path.join(self.root_dir(),
499 self._options.config_filename),
500 self.config_content)
501
502 @staticmethod
503 def LoadCurrentConfig(options):
504 """Searches for and loads a .gclient file relative to the current working
505 dir. Returns a GClient object."""
506 path = gclient_utils.FindGclientRoot(os.getcwd(), options.config_filename)
507 if not path:
508 return None
509 client = GClient(path, options)
510 client.SetConfig(gclient_utils.FileRead(
511 os.path.join(path, options.config_filename)))
512 return client
513
514 def SetDefaultConfig(self, solution_name, solution_url, safesync_url):
515 self.SetConfig(self.DEFAULT_CLIENT_FILE_TEXT % {
516 'solution_name': solution_name,
517 'solution_url': solution_url,
518 'safesync_url' : safesync_url,
519 })
520
521 def _SaveEntries(self, entries):
522 """Creates a .gclient_entries file to record the list of unique checkouts.
523
524 The .gclient_entries file lives in the same directory as .gclient.
525
526 Args:
527 entries: A sequence of solution names.
528 """
529 # Sometimes pprint.pformat will use {', sometimes it'll use { ' ... It
530 # makes testing a bit too fun.
531 result = pprint.pformat(entries, 2)
532 if result.startswith('{\''):
533 result = '{ \'' + result[2:]
534 text = "entries = \\\n" + result + '\n'
535 file_path = os.path.join(self.root_dir(), self._options.entries_filename)
536 gclient_utils.FileWrite(file_path, text)
537
538 def _ReadEntries(self):
539 """Read the .gclient_entries file for the given client.
540
541 Returns:
542 A sequence of solution names, which will be empty if there is the
543 entries file hasn't been created yet.
544 """
545 scope = {}
546 filename = os.path.join(self.root_dir(), self._options.entries_filename)
547 if not os.path.exists(filename):
548 return []
549 exec(gclient_utils.FileRead(filename), scope)
550 return scope['entries']
551
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000552 def _EnforceRevisions(self):
maruel@chromium.org918a9ae2010-05-28 15:50:30 +0000553 """Checks for revision overrides."""
554 revision_overrides = {}
maruel@chromium.org307d1792010-05-31 20:03:13 +0000555 if self._options.head:
556 return revision_overrides
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000557 for s in self.dependencies:
558 if not s.safesync_url:
maruel@chromium.org307d1792010-05-31 20:03:13 +0000559 continue
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000560 handle = urllib.urlopen(s.safesync_url)
maruel@chromium.org307d1792010-05-31 20:03:13 +0000561 rev = handle.read().strip()
562 handle.close()
563 if len(rev):
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000564 self._options.revisions.append('%s@%s' % (s.name, rev))
maruel@chromium.org307d1792010-05-31 20:03:13 +0000565 if not self._options.revisions:
566 return revision_overrides
567 # --revision will take over safesync_url.
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000568 solutions_names = [s.name for s in self.dependencies]
maruel@chromium.org307d1792010-05-31 20:03:13 +0000569 index = 0
570 for revision in self._options.revisions:
571 if not '@' in revision:
572 # Support for --revision 123
573 revision = '%s@%s' % (solutions_names[index], revision)
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000574 sol, rev = revision.split('@', 1)
maruel@chromium.org307d1792010-05-31 20:03:13 +0000575 if not sol in solutions_names:
576 #raise gclient_utils.Error('%s is not a valid solution.' % sol)
577 print >> sys.stderr, ('Please fix your script, having invalid '
578 '--revision flags will soon considered an error.')
579 else:
maruel@chromium.org918a9ae2010-05-28 15:50:30 +0000580 revision_overrides[sol] = rev
maruel@chromium.org307d1792010-05-31 20:03:13 +0000581 index += 1
maruel@chromium.org918a9ae2010-05-28 15:50:30 +0000582 return revision_overrides
583
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000584 def RunOnDeps(self, command, args):
585 """Runs a command on each dependency in a client and its dependencies.
586
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000587 Args:
588 command: The command to use (e.g., 'status' or 'diff')
589 args: list of str - extra arguments to add to the command line.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000590 """
maruel@chromium.org116704f2010-06-11 17:34:38 +0000591 if not command in self.SUPPORTED_COMMANDS:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000592 raise gclient_utils.Error("'%s' is an unsupported command" % command)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000593
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000594 if not self.dependencies:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000595 raise gclient_utils.Error("No solution specified")
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000596 revision_overrides = self._EnforceRevisions()
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000597
598 # When running runhooks --force, there's no need to consult the SCM.
599 # All known hooks are expected to run unconditionally regardless of working
600 # copy state, so skip the SCM status check.
601 run_scm = not (command == 'runhooks' and self._options.force)
602
603 entries = {}
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000604 file_list = []
605 # Run on the base solutions first.
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000606 for solution in self.dependencies:
607 name = solution.name
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000608 if name in entries:
609 raise gclient_utils.Error("solution %s specified more than once" % name)
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000610 url = solution.url
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000611 entries[name] = url
612 if run_scm and url:
613 self._options.revision = revision_overrides.get(name)
maruel@chromium.org75a59272010-06-11 22:34:03 +0000614 scm = gclient_scm.CreateSCM(url, self.root_dir(), name)
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000615 scm.RunCommand(command, self._options, args, file_list)
616 file_list = [os.path.join(name, f.strip()) for f in file_list]
617 self._options.revision = None
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000618
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000619 # Process the dependencies next (sort alphanumerically to ensure that
620 # containing directories get populated first and for readability)
maruel@chromium.org271375b2010-06-23 19:17:38 +0000621 deps = self._ParseAllDeps(entries)
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000622 deps_to_process = deps.keys()
623 deps_to_process.sort()
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000624
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000625 # First pass for direct dependencies.
626 if command == 'update' and not self._options.verbose:
627 pm = Progress('Syncing projects', len(deps_to_process))
628 for d in deps_to_process:
nasser@codeaurora.org1f7a3d12010-02-04 15:11:50 +0000629 if command == 'update' and not self._options.verbose:
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000630 pm.update()
631 if type(deps[d]) == str:
632 url = deps[d]
633 entries[d] = url
634 if run_scm:
635 self._options.revision = revision_overrides.get(d)
maruel@chromium.org75a59272010-06-11 22:34:03 +0000636 scm = gclient_scm.CreateSCM(url, self.root_dir(), d)
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000637 scm.RunCommand(command, self._options, args, file_list)
638 self._options.revision = None
639 elif isinstance(deps[d], self.FileImpl):
maruel@chromium.org491c04b2010-05-17 18:17:44 +0000640 file_dep = deps[d]
641 self._options.revision = file_dep.GetRevision()
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000642 if run_scm:
maruel@chromium.org75a59272010-06-11 22:34:03 +0000643 scm = gclient_scm.CreateSCM(file_dep.GetPath(), self.root_dir(), d)
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000644 scm.RunCommand("updatesingle", self._options,
maruel@chromium.org491c04b2010-05-17 18:17:44 +0000645 args + [file_dep.GetFilename()], file_list)
maruel@chromium.org79692d62010-05-14 18:57:13 +0000646
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000647 if command == 'update' and not self._options.verbose:
648 pm.end()
piman@chromium.org6f363722010-04-27 00:41:09 +0000649
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000650 # Second pass for inherited deps (via the From keyword)
651 for d in deps_to_process:
652 if isinstance(deps[d], self.FromImpl):
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000653 # Getting the URL from the sub_deps file can involve having to resolve
654 # a File() or having to resolve a relative URL. To resolve relative
655 # URLs, we need to pass in the orignal sub deps URL.
656 sub_deps_base_url = deps[deps[d].module_name]
maruel@chromium.org271375b2010-06-23 19:17:38 +0000657 sub_deps = Dependency(self, deps[d].module_name, sub_deps_base_url
658 ).ParseDepsFile(False)
maruel@chromium.org75a59272010-06-11 22:34:03 +0000659 url = deps[d].GetUrl(d, sub_deps_base_url, self.root_dir(), sub_deps)
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000660 entries[d] = url
661 if run_scm:
662 self._options.revision = revision_overrides.get(d)
maruel@chromium.org75a59272010-06-11 22:34:03 +0000663 scm = gclient_scm.CreateSCM(url, self.root_dir(), d)
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000664 scm.RunCommand(command, self._options, args, file_list)
665 self._options.revision = None
gspencer@google.comdf2d5902009-09-11 22:16:21 +0000666
phajdan.jr@chromium.orgd83b2b22009-08-11 15:30:55 +0000667 # Convert all absolute paths to relative.
668 for i in range(len(file_list)):
669 # TODO(phajdan.jr): We should know exactly when the paths are absolute.
670 # It depends on the command being executed (like runhooks vs sync).
671 if not os.path.isabs(file_list[i]):
672 continue
673
maruel@chromium.org75a59272010-06-11 22:34:03 +0000674 prefix = os.path.commonprefix([self.root_dir().lower(),
phajdan.jr@chromium.orgd83b2b22009-08-11 15:30:55 +0000675 file_list[i].lower()])
676 file_list[i] = file_list[i][len(prefix):]
677
678 # Strip any leading path separators.
679 while file_list[i].startswith('\\') or file_list[i].startswith('/'):
680 file_list[i] = file_list[i][1:]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000681
maruel@chromium.org75a59272010-06-11 22:34:03 +0000682 is_using_git = gclient_utils.IsUsingGit(self.root_dir(), entries.keys())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000683 self._RunHooks(command, file_list, is_using_git)
684
685 if command == 'update':
ajwong@chromium.orgcdcee802009-06-23 15:30:42 +0000686 # Notify the user if there is an orphaned entry in their working copy.
687 # Only delete the directory if there are no changes in it, and
688 # delete_unversioned_trees is set to true.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000689 prev_entries = self._ReadEntries()
690 for entry in prev_entries:
maruel@chromium.orgc5e9aec2009-08-03 18:25:56 +0000691 # Fix path separator on Windows.
692 entry_fixed = entry.replace('/', os.path.sep)
maruel@chromium.org75a59272010-06-11 22:34:03 +0000693 e_dir = os.path.join(self.root_dir(), entry_fixed)
maruel@chromium.orgc5e9aec2009-08-03 18:25:56 +0000694 # Use entry and not entry_fixed there.
maruel@chromium.org0329e672009-05-13 18:41:04 +0000695 if entry not in entries and os.path.exists(e_dir):
msb@chromium.org83017012009-09-28 18:52:12 +0000696 modified_files = False
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000697 if isinstance(prev_entries, list):
msb@chromium.org83017012009-09-28 18:52:12 +0000698 # old .gclient_entries format was list, now dict
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000699 modified_files = gclient_scm.scm.SVN.CaptureStatus(e_dir)
msb@chromium.org83017012009-09-28 18:52:12 +0000700 else:
701 file_list = []
maruel@chromium.org75a59272010-06-11 22:34:03 +0000702 scm = gclient_scm.CreateSCM(prev_entries[entry], self.root_dir(),
msb@chromium.org83017012009-09-28 18:52:12 +0000703 entry_fixed)
704 scm.status(self._options, [], file_list)
705 modified_files = file_list != []
706 if not self._options.delete_unversioned_trees or modified_files:
maruel@chromium.orgc5e9aec2009-08-03 18:25:56 +0000707 # There are modified files in this entry. Keep warning until
708 # removed.
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000709 print(('\nWARNING: \'%s\' is no longer part of this client. '
710 'It is recommended that you manually remove it.\n') %
maruel@chromium.orgc5e9aec2009-08-03 18:25:56 +0000711 entry_fixed)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000712 else:
713 # Delete the entry
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000714 print('\n________ deleting \'%s\' ' +
715 'in \'%s\'') % (entry_fixed, self.root_dir())
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000716 gclient_utils.RemoveDirectory(e_dir)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000717 # record the current list of entries for next time
718 self._SaveEntries(entries)
maruel@chromium.org17cdf762010-05-28 17:30:52 +0000719 return 0
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000720
721 def PrintRevInfo(self):
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000722 """Output revision info mapping for the client and its dependencies.
723
724 This allows the capture of an overall "revision" for the source tree that
maruel@chromium.org918a9ae2010-05-28 15:50:30 +0000725 can be used to reproduce the same tree in the future. It is only useful for
726 "unpinned dependencies", i.e. DEPS/deps references without a svn revision
727 number or a git hash. A git branch name isn't "pinned" since the actual
728 commit can change.
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000729
730 The --snapshot option allows creating a .gclient file to reproduce the tree.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000731 """
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000732 if not self.dependencies:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000733 raise gclient_utils.Error("No solution specified")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000734
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000735 # Inner helper to generate base url and rev tuple
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000736 def GetURLAndRev(name, original_url):
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000737 url, _ = gclient_utils.SplitUrlRevision(original_url)
maruel@chromium.org75a59272010-06-11 22:34:03 +0000738 scm = gclient_scm.CreateSCM(original_url, self.root_dir(), name)
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000739 return (url, scm.revinfo(self._options, [], None))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000740
maruel@chromium.orge3da35f2010-03-09 21:40:45 +0000741 # text of the snapshot gclient file
742 new_gclient = ""
743 # Dictionary of { path : SCM url } to ensure no duplicate solutions
744 solution_names = {}
nasser@codeaurora.orgde8f3522010-03-11 23:47:44 +0000745 entries = {}
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000746 # Run on the base solutions first.
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000747 for solution in self.dependencies:
maruel@chromium.orge3da35f2010-03-09 21:40:45 +0000748 # Dictionary of { path : SCM url } to describe the gclient checkout
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000749 name = solution.name
maruel@chromium.orge3da35f2010-03-09 21:40:45 +0000750 if name in solution_names:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000751 raise gclient_utils.Error("solution %s specified more than once" % name)
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000752 (url, rev) = GetURLAndRev(name, solution.url)
msb@chromium.org770ff9e2009-09-23 17:18:18 +0000753 entries[name] = "%s@%s" % (url, rev)
maruel@chromium.orge3da35f2010-03-09 21:40:45 +0000754 solution_names[name] = "%s@%s" % (url, rev)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000755
nasser@codeaurora.orgde8f3522010-03-11 23:47:44 +0000756 # Process the dependencies next (sort alphanumerically to ensure that
757 # containing directories get populated first and for readability)
maruel@chromium.org271375b2010-06-23 19:17:38 +0000758 deps = self._ParseAllDeps(entries)
nasser@codeaurora.orgde8f3522010-03-11 23:47:44 +0000759 deps_to_process = deps.keys()
760 deps_to_process.sort()
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000761
nasser@codeaurora.orgde8f3522010-03-11 23:47:44 +0000762 # First pass for direct dependencies.
763 for d in deps_to_process:
764 if type(deps[d]) == str:
765 (url, rev) = GetURLAndRev(d, deps[d])
766 entries[d] = "%s@%s" % (url, rev)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000767
nasser@codeaurora.orgde8f3522010-03-11 23:47:44 +0000768 # Second pass for inherited deps (via the From keyword)
769 for d in deps_to_process:
tony@chromium.org4b5b1772010-04-08 01:52:56 +0000770 if isinstance(deps[d], self.FromImpl):
nasser@codeaurora.orgde8f3522010-03-11 23:47:44 +0000771 deps_parent_url = entries[deps[d].module_name]
772 if deps_parent_url.find("@") < 0:
773 raise gclient_utils.Error("From %s missing revisioned url" %
774 deps[d].module_name)
maruel@chromium.org271375b2010-06-23 19:17:38 +0000775 sub_deps_base_url = deps[deps[d].module_name]
776 sub_deps = Dependency(self, deps[d].module_name, sub_deps_base_url
777 ).ParseDepsFile(False)
778 url = deps[d].GetUrl(d, sub_deps_base_url, self.root_dir(), sub_deps)
779 (url, rev) = GetURLAndRev(d, url)
nasser@codeaurora.orgde8f3522010-03-11 23:47:44 +0000780 entries[d] = "%s@%s" % (url, rev)
maruel@chromium.orge3da35f2010-03-09 21:40:45 +0000781
nasser@codeaurora.orgde8f3522010-03-11 23:47:44 +0000782 # Build the snapshot configuration string
783 if self._options.snapshot:
784 url = entries.pop(name)
785 custom_deps = ",\n ".join(["\"%s\": \"%s\"" % (x, entries[x])
786 for x in sorted(entries.keys())])
maruel@chromium.orge3da35f2010-03-09 21:40:45 +0000787
maruel@chromium.org1f7d1182010-05-17 18:17:38 +0000788 new_gclient += self.DEFAULT_SNAPSHOT_SOLUTION_TEXT % {
nasser@codeaurora.orgde8f3522010-03-11 23:47:44 +0000789 'solution_name': name,
790 'solution_url': url,
791 'safesync_url' : "",
792 'solution_deps': custom_deps,
793 }
794 else:
795 print(";\n".join(["%s: %s" % (x, entries[x])
796 for x in sorted(entries.keys())]))
maruel@chromium.orge3da35f2010-03-09 21:40:45 +0000797
798 # Print the snapshot configuration file
799 if self._options.snapshot:
maruel@chromium.org491c04b2010-05-17 18:17:44 +0000800 config = self.DEFAULT_SNAPSHOT_FILE_TEXT % {'solution_list': new_gclient}
maruel@chromium.org75a59272010-06-11 22:34:03 +0000801 snapclient = GClient(self.root_dir(), self._options)
maruel@chromium.orge3da35f2010-03-09 21:40:45 +0000802 snapclient.SetConfig(config)
maruel@chromium.org116704f2010-06-11 17:34:38 +0000803 print(snapclient.config_content)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000804
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000805 def ParseDepsFile(self, direct_reference):
806 """No DEPS to parse for a .gclient file."""
807 self.direct_reference = direct_reference
808 self.deps_parsed = True
809
maruel@chromium.org75a59272010-06-11 22:34:03 +0000810 def root_dir(self):
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000811 """Root directory of gclient checkout."""
maruel@chromium.org75a59272010-06-11 22:34:03 +0000812 return self._root_dir
813
maruel@chromium.org271375b2010-06-23 19:17:38 +0000814 def enforced_os(self):
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000815 """What deps_os entries that are to be parsed."""
maruel@chromium.org271375b2010-06-23 19:17:38 +0000816 return self._enforced_os
817
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000818 def recursion_limit(self):
819 """How recursive can each dependencies in DEPS file can load DEPS file."""
820 return self._recursion_limit
821
822 def tree(self, force_all):
823 """Returns a flat list of all the dependencies."""
824 def subtree(dep):
825 if not force_all and not dep.direct_reference:
826 # Was loaded from a From() keyword in a DEPS file, don't load all its
827 # dependencies.
828 return []
829 result = dep.dependencies[:]
830 for d in dep.dependencies:
831 result.extend(subtree(d))
832 return result
833 return subtree(self)
834
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000835
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000836#### gclient commands.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000837
838
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000839def CMDcleanup(parser, args):
840 """Cleans up all working copies.
maruel@chromium.orgddff62d2010-05-17 21:02:36 +0000841
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000842Mostly svn-specific. Simply runs 'svn cleanup' for each module.
maruel@chromium.org79692d62010-05-14 18:57:13 +0000843"""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000844 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
845 help='override deps for the specified (comma-separated) '
846 'platform(s); \'all\' will process all deps_os '
847 'references')
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000848 (options, args) = parser.parse_args(args)
maruel@chromium.org2806acc2009-05-15 12:33:34 +0000849 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000850 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000851 raise gclient_utils.Error('client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000852 if options.verbose:
853 # Print out the .gclient file. This is longer than if we just printed the
854 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.org116704f2010-06-11 17:34:38 +0000855 print(client.config_content)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000856 return client.RunOnDeps('cleanup', args)
857
858
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000859@attr('usage', '[url] [safesync url]')
860def CMDconfig(parser, args):
maruel@chromium.orgddff62d2010-05-17 21:02:36 +0000861 """Create a .gclient file in the current directory.
862
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000863This specifies the configuration for further commands. After update/sync,
maruel@chromium.org79692d62010-05-14 18:57:13 +0000864top-level DEPS files in each module are read to determine dependent
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000865modules to operate on as well. If optional [url] parameter is
maruel@chromium.org79692d62010-05-14 18:57:13 +0000866provided, then configuration is read from a specified Subversion server
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000867URL.
maruel@chromium.org79692d62010-05-14 18:57:13 +0000868"""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000869 parser.add_option('--spec',
870 help='create a gclient file containing the provided '
871 'string. Due to Cygwin/Python brokenness, it '
872 'probably can\'t contain any newlines.')
873 parser.add_option('--name',
874 help='overrides the default name for the solution')
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000875 (options, args) = parser.parse_args(args)
maruel@chromium.org5fc2a332010-05-26 19:37:15 +0000876 if ((options.spec and args) or len(args) > 2 or
877 (not options.spec and not args)):
878 parser.error('Inconsistent arguments. Use either --spec or one or 2 args')
879
maruel@chromium.org0329e672009-05-13 18:41:04 +0000880 if os.path.exists(options.config_filename):
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000881 raise gclient_utils.Error('%s file already exists in the current directory'
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000882 % options.config_filename)
maruel@chromium.org2806acc2009-05-15 12:33:34 +0000883 client = GClient('.', options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000884 if options.spec:
885 client.SetConfig(options.spec)
886 else:
maruel@chromium.org1ab7ffc2009-06-03 17:21:37 +0000887 base_url = args[0].rstrip('/')
iposva@chromium.org8cf7a392010-04-07 17:20:26 +0000888 if not options.name:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000889 name = base_url.split('/')[-1]
iposva@chromium.org8cf7a392010-04-07 17:20:26 +0000890 else:
891 # specify an alternate relpath for the given URL.
892 name = options.name
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000893 safesync_url = ''
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000894 if len(args) > 1:
895 safesync_url = args[1]
896 client.SetDefaultConfig(name, base_url, safesync_url)
897 client.SaveConfig()
maruel@chromium.org79692d62010-05-14 18:57:13 +0000898 return 0
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000899
900
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000901def CMDexport(parser, args):
maruel@chromium.orgddff62d2010-05-17 21:02:36 +0000902 """Wrapper for svn export for all managed directories."""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000903 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
904 help='override deps for the specified (comma-separated) '
905 'platform(s); \'all\' will process all deps_os '
906 'references')
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000907 (options, args) = parser.parse_args(args)
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +0000908 if len(args) != 1:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000909 raise gclient_utils.Error('Need directory name')
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +0000910 client = GClient.LoadCurrentConfig(options)
911
912 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000913 raise gclient_utils.Error('client not configured; see \'gclient config\'')
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +0000914
915 if options.verbose:
916 # Print out the .gclient file. This is longer than if we just printed the
917 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.org116704f2010-06-11 17:34:38 +0000918 print(client.config_content)
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +0000919 return client.RunOnDeps('export', args)
920
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000921
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000922@attr('epilog', """Example:
923 gclient pack > patch.txt
924 generate simple patch for configured client and dependences
925""")
926def CMDpack(parser, args):
maruel@chromium.org79692d62010-05-14 18:57:13 +0000927 """Generate a patch which can be applied at the root of the tree.
maruel@chromium.orgddff62d2010-05-17 21:02:36 +0000928
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000929Internally, runs 'svn diff'/'git diff' on each checked out module and
maruel@chromium.org79692d62010-05-14 18:57:13 +0000930dependencies, and performs minimal postprocessing of the output. The
931resulting patch is printed to stdout and can be applied to a freshly
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000932checked out tree via 'patch -p0 < patchfile'.
maruel@chromium.org79692d62010-05-14 18:57:13 +0000933"""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000934 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
935 help='override deps for the specified (comma-separated) '
936 'platform(s); \'all\' will process all deps_os '
937 'references')
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000938 (options, args) = parser.parse_args(args)
kbr@google.comab318592009-09-04 00:54:55 +0000939 client = GClient.LoadCurrentConfig(options)
940 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000941 raise gclient_utils.Error('client not configured; see \'gclient config\'')
kbr@google.comab318592009-09-04 00:54:55 +0000942 if options.verbose:
943 # Print out the .gclient file. This is longer than if we just printed the
944 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.org116704f2010-06-11 17:34:38 +0000945 print(client.config_content)
kbr@google.comab318592009-09-04 00:54:55 +0000946 return client.RunOnDeps('pack', args)
947
948
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000949def CMDstatus(parser, args):
950 """Show modification status for every dependencies."""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000951 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
952 help='override deps for the specified (comma-separated) '
953 'platform(s); \'all\' will process all deps_os '
954 'references')
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000955 (options, args) = parser.parse_args(args)
maruel@chromium.org2806acc2009-05-15 12:33:34 +0000956 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000957 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000958 raise gclient_utils.Error('client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000959 if options.verbose:
960 # Print out the .gclient file. This is longer than if we just printed the
961 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.org116704f2010-06-11 17:34:38 +0000962 print(client.config_content)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000963 return client.RunOnDeps('status', args)
964
965
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000966@attr('epilog', """Examples:
maruel@chromium.org79692d62010-05-14 18:57:13 +0000967 gclient sync
968 update files from SCM according to current configuration,
969 *for modules which have changed since last update or sync*
970 gclient sync --force
971 update files from SCM according to current configuration, for
972 all modules (useful for recovering files deleted from local copy)
973 gclient sync --revision src@31000
974 update src directory to r31000
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000975""")
976def CMDsync(parser, args):
977 """Checkout/update all modules."""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000978 parser.add_option('-f', '--force', action='store_true',
979 help='force update even for unchanged modules')
980 parser.add_option('-n', '--nohooks', action='store_true',
981 help='don\'t run hooks after the update is complete')
982 parser.add_option('-r', '--revision', action='append',
983 dest='revisions', metavar='REV', default=[],
984 help='Enforces revision/hash for the solutions with the '
985 'format src@rev. The src@ part is optional and can be '
986 'skipped. -r can be used multiple times when .gclient '
987 'has multiple solutions configured and will work even '
988 'if the src@ part is skipped.')
989 parser.add_option('-H', '--head', action='store_true',
990 help='skips any safesync_urls specified in '
991 'configured solutions and sync to head instead')
992 parser.add_option('-D', '--delete_unversioned_trees', action='store_true',
993 help='delete any unexpected unversioned trees '
994 'that are in the checkout')
995 parser.add_option('-R', '--reset', action='store_true',
996 help='resets any local changes before updating (git only)')
997 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
998 help='override deps for the specified (comma-separated) '
999 'platform(s); \'all\' will process all deps_os '
1000 'references')
1001 parser.add_option('-m', '--manually_grab_svn_rev', action='store_true',
1002 help='Skip svn up whenever possible by requesting '
1003 'actual HEAD revision from the repository')
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001004 (options, args) = parser.parse_args(args)
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001005 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001006
1007 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00001008 raise gclient_utils.Error('client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001009
maruel@chromium.org307d1792010-05-31 20:03:13 +00001010 if options.revisions and options.head:
1011 # TODO(maruel): Make it a parser.error if it doesn't break any builder.
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00001012 print('Warning: you cannot use both --head and --revision')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001013
1014 if options.verbose:
1015 # Print out the .gclient file. This is longer than if we just printed the
1016 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.org116704f2010-06-11 17:34:38 +00001017 print(client.config_content)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001018 return client.RunOnDeps('update', args)
1019
1020
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001021def CMDupdate(parser, args):
maruel@chromium.orgddff62d2010-05-17 21:02:36 +00001022 """Alias for the sync command. Deprecated."""
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001023 return CMDsync(parser, args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001024
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001025def CMDdiff(parser, args):
1026 """Displays local diff for every dependencies."""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00001027 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
1028 help='override deps for the specified (comma-separated) '
1029 'platform(s); \'all\' will process all deps_os '
1030 'references')
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001031 (options, args) = parser.parse_args(args)
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001032 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001033 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00001034 raise gclient_utils.Error('client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001035 if options.verbose:
1036 # Print out the .gclient file. This is longer than if we just printed the
1037 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.org116704f2010-06-11 17:34:38 +00001038 print(client.config_content)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001039 return client.RunOnDeps('diff', args)
1040
1041
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001042def CMDrevert(parser, args):
1043 """Revert all modifications in every dependencies."""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00001044 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
1045 help='override deps for the specified (comma-separated) '
1046 'platform(s); \'all\' will process all deps_os '
1047 'references')
1048 parser.add_option('-n', '--nohooks', action='store_true',
1049 help='don\'t run hooks after the revert is complete')
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001050 (options, args) = parser.parse_args(args)
1051 # --force is implied.
1052 options.force = True
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001053 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001054 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00001055 raise gclient_utils.Error('client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001056 return client.RunOnDeps('revert', args)
1057
1058
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001059def CMDrunhooks(parser, args):
1060 """Runs hooks for files that have been modified in the local working copy."""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00001061 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
1062 help='override deps for the specified (comma-separated) '
1063 'platform(s); \'all\' will process all deps_os '
1064 'references')
1065 parser.add_option('-f', '--force', action='store_true', default=True,
1066 help='Deprecated. No effect.')
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001067 (options, args) = parser.parse_args(args)
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001068 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001069 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00001070 raise gclient_utils.Error('client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001071 if options.verbose:
1072 # Print out the .gclient file. This is longer than if we just printed the
1073 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.org116704f2010-06-11 17:34:38 +00001074 print(client.config_content)
maruel@chromium.org5df6a462009-08-28 18:52:26 +00001075 options.force = True
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001076 options.nohooks = False
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001077 return client.RunOnDeps('runhooks', args)
1078
1079
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001080def CMDrevinfo(parser, args):
maruel@chromium.org9eda4112010-06-11 18:56:10 +00001081 """Output revision info mapping for the client and its dependencies.
1082
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00001083 This allows the capture of an overall 'revision' for the source tree that
maruel@chromium.org9eda4112010-06-11 18:56:10 +00001084 can be used to reproduce the same tree in the future. It is only useful for
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00001085 'unpinned dependencies', i.e. DEPS/deps references without a svn revision
1086 number or a git hash. A git branch name isn't 'pinned' since the actual
maruel@chromium.org9eda4112010-06-11 18:56:10 +00001087 commit can change.
1088 """
1089 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
1090 help='override deps for the specified (comma-separated) '
1091 'platform(s); \'all\' will process all deps_os '
1092 'references')
1093 parser.add_option('-s', '--snapshot', action='store_true',
1094 help='creates a snapshot .gclient file of the current '
1095 'version of all repositories to reproduce the tree, '
1096 'implies -a')
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001097 (options, args) = parser.parse_args(args)
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001098 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001099 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00001100 raise gclient_utils.Error('client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001101 client.PrintRevInfo()
maruel@chromium.org79692d62010-05-14 18:57:13 +00001102 return 0
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001103
1104
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001105def Command(name):
1106 return getattr(sys.modules[__name__], 'CMD' + name, None)
1107
1108
1109def CMDhelp(parser, args):
1110 """Prints list of commands or help for a specific command."""
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001111 (_, args) = parser.parse_args(args)
maruel@chromium.orgddff62d2010-05-17 21:02:36 +00001112 if len(args) == 1:
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001113 return Main(args + ['--help'])
maruel@chromium.orgddff62d2010-05-17 21:02:36 +00001114 parser.print_help()
1115 return 0
1116
1117
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001118def GenUsage(parser, command):
1119 """Modify an OptParse object with the function's documentation."""
1120 obj = Command(command)
1121 if command == 'help':
1122 command = '<command>'
1123 # OptParser.description prefer nicely non-formatted strings.
1124 parser.description = re.sub('[\r\n ]{2,}', ' ', obj.__doc__)
1125 usage = getattr(obj, 'usage', '')
1126 parser.set_usage('%%prog %s [options] %s' % (command, usage))
1127 parser.epilog = getattr(obj, 'epilog', None)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001128
1129
1130def Main(argv):
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001131 """Doesn't parse the arguments here, just find the right subcommand to
1132 execute."""
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001133 try:
1134 # Do it late so all commands are listed.
1135 CMDhelp.usage = ('\n\nCommands are:\n' + '\n'.join([
1136 ' %-10s %s' % (fn[3:], Command(fn[3:]).__doc__.split('\n')[0].strip())
1137 for fn in dir(sys.modules[__name__]) if fn.startswith('CMD')]))
1138 parser = optparse.OptionParser(version='%prog ' + __version__)
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001139 parser.add_option('-v', '--verbose', action='count', default=0,
1140 help='Produces additional output for diagnostics. Can be '
1141 'used up to three times for more logging info.')
1142 parser.add_option('--gclientfile', dest='config_filename',
1143 default=os.environ.get('GCLIENT_FILE', '.gclient'),
1144 help='Specify an alternate %default file')
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001145 # Integrate standard options processing.
1146 old_parser = parser.parse_args
1147 def Parse(args):
1148 (options, args) = old_parser(args)
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001149 level = None
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001150 if options.verbose == 2:
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001151 level = logging.INFO
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001152 elif options.verbose > 2:
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001153 level = logging.DEBUG
1154 logging.basicConfig(level=level,
1155 format='%(module)s(%(lineno)d) %(funcName)s:%(message)s')
1156 options.entries_filename = options.config_filename + '_entries'
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001157 if not hasattr(options, 'revisions'):
1158 # GClient.RunOnDeps expects it even if not applicable.
1159 options.revisions = []
1160 if not hasattr(options, 'head'):
1161 options.head = None
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001162 if not hasattr(options, 'nohooks'):
1163 options.nohooks = True
1164 if not hasattr(options, 'deps_os'):
1165 options.deps_os = None
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001166 return (options, args)
1167 parser.parse_args = Parse
1168 # We don't want wordwrapping in epilog (usually examples)
1169 parser.format_epilog = lambda _: parser.epilog or ''
1170 if argv:
1171 command = Command(argv[0])
1172 if command:
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001173 # 'fix' the usage and the description now that we know the subcommand.
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001174 GenUsage(parser, argv[0])
1175 return command(parser, argv[1:])
1176 # Not a known command. Default to help.
1177 GenUsage(parser, 'help')
1178 return CMDhelp(parser, argv)
1179 except gclient_utils.Error, e:
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001180 print >> sys.stderr, 'Error: %s' % str(e)
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001181 return 1
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001182
1183
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001184if '__main__' == __name__:
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001185 sys.exit(Main(sys.argv[1:]))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001186
1187# vim: ts=2:sw=2:tw=80:et: