blob: 3a5a669bb35be80a1196a054342f29235136090b [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@google.comfb2b8eb2009-04-23 21:03:42 +0000314 """Runs the action from a single hook.
315 """
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000316 logging.info(hook_dict)
317 logging.info(matching_file_list)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000318 command = hook_dict['action'][:]
319 if command[0] == 'python':
320 # If the hook specified "python" as the first item, the action is a
321 # Python script. Run it by starting a new copy of the same
322 # interpreter.
323 command[0] = sys.executable
324
phajdan.jr@chromium.org71b40682009-07-31 23:40:09 +0000325 if '$matching_files' in command:
phajdan.jr@chromium.org68f2e092009-08-06 17:05:35 +0000326 splice_index = command.index('$matching_files')
327 command[splice_index:splice_index + 1] = matching_file_list
phajdan.jr@chromium.org71b40682009-07-31 23:40:09 +0000328
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000329 # Use a discrete exit status code of 2 to indicate that a hook action
330 # failed. Users of this script may wish to treat hook action failures
331 # differently from VC failures.
maruel@chromium.org75a59272010-06-11 22:34:03 +0000332 gclient_utils.SubprocessCall(command, self.root_dir(), fail_status=2)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000333
334 def _RunHooks(self, command, file_list, is_using_git):
335 """Evaluates all hooks, running actions as needed.
336 """
337 # Hooks only run for these command types.
338 if not command in ('update', 'revert', 'runhooks'):
339 return
340
evan@chromium.org67820ef2009-07-27 17:23:00 +0000341 # Hooks only run when --nohooks is not specified
342 if self._options.nohooks:
343 return
344
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000345 # Get any hooks from the .gclient file.
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000346 hooks = self.deps_hooks[:]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000347 # Add any hooks found in DEPS files.
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000348 for d in self.dependencies:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000349 hooks.extend(d.deps_hooks)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000350
351 # If "--force" was specified, run all hooks regardless of what files have
352 # changed. If the user is using git, then we don't know what files have
353 # changed so we always run all hooks.
354 if self._options.force or is_using_git:
355 for hook_dict in hooks:
phajdan.jr@chromium.org71b40682009-07-31 23:40:09 +0000356 self._RunHookAction(hook_dict, [])
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000357 return
358
359 # Run hooks on the basis of whether the files from the gclient operation
360 # match each hook's pattern.
361 for hook_dict in hooks:
362 pattern = re.compile(hook_dict['pattern'])
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000363 matching_file_list = [f for f in file_list if pattern.search(f)]
phajdan.jr@chromium.org71b40682009-07-31 23:40:09 +0000364 if matching_file_list:
365 self._RunHookAction(hook_dict, matching_file_list)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000366
maruel@chromium.org271375b2010-06-23 19:17:38 +0000367 def root_dir(self):
368 return self.parent.root_dir()
369
370 def enforced_os(self):
371 return self.parent.enforced_os()
372
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000373
374class GClient(Dependency):
375 """Main gclient checkout root where .gclient resides."""
376 SUPPORTED_COMMANDS = [
377 'cleanup', 'diff', 'export', 'pack', 'revert', 'status', 'update',
378 'runhooks'
379 ]
380
381 DEPS_OS_CHOICES = {
382 "win32": "win",
383 "win": "win",
384 "cygwin": "win",
385 "darwin": "mac",
386 "mac": "mac",
387 "unix": "unix",
388 "linux": "unix",
389 "linux2": "unix",
390 }
391
392 DEFAULT_CLIENT_FILE_TEXT = ("""\
393solutions = [
394 { "name" : "%(solution_name)s",
395 "url" : "%(solution_url)s",
396 "custom_deps" : {
397 },
398 "safesync_url": "%(safesync_url)s"
399 },
400]
401""")
402
403 DEFAULT_SNAPSHOT_SOLUTION_TEXT = ("""\
404 { "name" : "%(solution_name)s",
405 "url" : "%(solution_url)s",
406 "custom_deps" : {
407 %(solution_deps)s,
408 },
409 "safesync_url": "%(safesync_url)s"
410 },
411""")
412
413 DEFAULT_SNAPSHOT_FILE_TEXT = ("""\
414# Snapshot generated with gclient revinfo --snapshot
415solutions = [
416%(solution_list)s
417]
418""")
419
420 def __init__(self, root_dir, options):
421 Dependency.__init__(self, None, None, None)
maruel@chromium.org0d425922010-06-21 19:22:24 +0000422 self._options = options
maruel@chromium.org271375b2010-06-23 19:17:38 +0000423 if options.deps_os:
424 enforced_os = options.deps_os.split(',')
425 else:
426 enforced_os = [self.DEPS_OS_CHOICES.get(sys.platform, 'unix')]
427 if 'all' in enforced_os:
428 enforced_os = self.DEPS_OS_CHOICES.itervalues()
429 self._enforced_os = list(set(enforced_os))
430 self._root_dir = root_dir
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000431 self.config_content = None
432
433 def SetConfig(self, content):
434 assert self.dependencies == []
435 config_dict = {}
436 self.config_content = content
437 try:
438 exec(content, config_dict)
439 except SyntaxError, e:
440 try:
441 # Try to construct a human readable error message
442 error_message = [
443 'There is a syntax error in your configuration file.',
444 'Line #%s, character %s:' % (e.lineno, e.offset),
445 '"%s"' % re.sub(r'[\r\n]*$', '', e.text) ]
446 except:
447 # Something went wrong, re-raise the original exception
448 raise e
449 else:
450 # Raise a new exception with the human readable message:
451 raise gclient_utils.Error('\n'.join(error_message))
452 for s in config_dict.get('solutions', []):
maruel@chromium.org81843b82010-06-28 16:49:26 +0000453 try:
454 self.dependencies.append(Dependency(
455 self, s['name'], s['url'],
456 s.get('safesync_url', None),
457 s.get('custom_deps', {}),
458 s.get('custom_vars', {})))
459 except KeyError:
460 raise gclient_utils.Error('Invalid .gclient file. Solution is '
461 'incomplete: %s' % s)
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000462 # .gclient can have hooks.
463 self.deps_hooks = config_dict.get('hooks', [])
464
465 def SaveConfig(self):
466 gclient_utils.FileWrite(os.path.join(self.root_dir(),
467 self._options.config_filename),
468 self.config_content)
469
470 @staticmethod
471 def LoadCurrentConfig(options):
472 """Searches for and loads a .gclient file relative to the current working
473 dir. Returns a GClient object."""
474 path = gclient_utils.FindGclientRoot(os.getcwd(), options.config_filename)
475 if not path:
476 return None
477 client = GClient(path, options)
478 client.SetConfig(gclient_utils.FileRead(
479 os.path.join(path, options.config_filename)))
480 return client
481
482 def SetDefaultConfig(self, solution_name, solution_url, safesync_url):
483 self.SetConfig(self.DEFAULT_CLIENT_FILE_TEXT % {
484 'solution_name': solution_name,
485 'solution_url': solution_url,
486 'safesync_url' : safesync_url,
487 })
488
489 def _SaveEntries(self, entries):
490 """Creates a .gclient_entries file to record the list of unique checkouts.
491
492 The .gclient_entries file lives in the same directory as .gclient.
493
494 Args:
495 entries: A sequence of solution names.
496 """
497 # Sometimes pprint.pformat will use {', sometimes it'll use { ' ... It
498 # makes testing a bit too fun.
499 result = pprint.pformat(entries, 2)
500 if result.startswith('{\''):
501 result = '{ \'' + result[2:]
502 text = "entries = \\\n" + result + '\n'
503 file_path = os.path.join(self.root_dir(), self._options.entries_filename)
504 gclient_utils.FileWrite(file_path, text)
505
506 def _ReadEntries(self):
507 """Read the .gclient_entries file for the given client.
508
509 Returns:
510 A sequence of solution names, which will be empty if there is the
511 entries file hasn't been created yet.
512 """
513 scope = {}
514 filename = os.path.join(self.root_dir(), self._options.entries_filename)
515 if not os.path.exists(filename):
516 return []
517 exec(gclient_utils.FileRead(filename), scope)
518 return scope['entries']
519
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000520 def _EnforceRevisions(self):
maruel@chromium.org918a9ae2010-05-28 15:50:30 +0000521 """Checks for revision overrides."""
522 revision_overrides = {}
maruel@chromium.org307d1792010-05-31 20:03:13 +0000523 if self._options.head:
524 return revision_overrides
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000525 for s in self.dependencies:
526 if not s.safesync_url:
maruel@chromium.org307d1792010-05-31 20:03:13 +0000527 continue
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000528 handle = urllib.urlopen(s.safesync_url)
maruel@chromium.org307d1792010-05-31 20:03:13 +0000529 rev = handle.read().strip()
530 handle.close()
531 if len(rev):
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000532 self._options.revisions.append('%s@%s' % (s.name, rev))
maruel@chromium.org307d1792010-05-31 20:03:13 +0000533 if not self._options.revisions:
534 return revision_overrides
535 # --revision will take over safesync_url.
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000536 solutions_names = [s.name for s in self.dependencies]
maruel@chromium.org307d1792010-05-31 20:03:13 +0000537 index = 0
538 for revision in self._options.revisions:
539 if not '@' in revision:
540 # Support for --revision 123
541 revision = '%s@%s' % (solutions_names[index], revision)
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000542 sol, rev = revision.split('@', 1)
maruel@chromium.org307d1792010-05-31 20:03:13 +0000543 if not sol in solutions_names:
544 #raise gclient_utils.Error('%s is not a valid solution.' % sol)
545 print >> sys.stderr, ('Please fix your script, having invalid '
546 '--revision flags will soon considered an error.')
547 else:
maruel@chromium.org918a9ae2010-05-28 15:50:30 +0000548 revision_overrides[sol] = rev
maruel@chromium.org307d1792010-05-31 20:03:13 +0000549 index += 1
maruel@chromium.org918a9ae2010-05-28 15:50:30 +0000550 return revision_overrides
551
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000552 def RunOnDeps(self, command, args):
553 """Runs a command on each dependency in a client and its dependencies.
554
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000555 Args:
556 command: The command to use (e.g., 'status' or 'diff')
557 args: list of str - extra arguments to add to the command line.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000558 """
maruel@chromium.org116704f2010-06-11 17:34:38 +0000559 if not command in self.SUPPORTED_COMMANDS:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000560 raise gclient_utils.Error("'%s' is an unsupported command" % command)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000561
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000562 if not self.dependencies:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000563 raise gclient_utils.Error("No solution specified")
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000564 revision_overrides = self._EnforceRevisions()
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000565
566 # When running runhooks --force, there's no need to consult the SCM.
567 # All known hooks are expected to run unconditionally regardless of working
568 # copy state, so skip the SCM status check.
569 run_scm = not (command == 'runhooks' and self._options.force)
570
571 entries = {}
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000572 file_list = []
573 # Run on the base solutions first.
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000574 for solution in self.dependencies:
575 name = solution.name
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000576 if name in entries:
577 raise gclient_utils.Error("solution %s specified more than once" % name)
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000578 url = solution.url
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000579 entries[name] = url
580 if run_scm and url:
581 self._options.revision = revision_overrides.get(name)
maruel@chromium.org75a59272010-06-11 22:34:03 +0000582 scm = gclient_scm.CreateSCM(url, self.root_dir(), name)
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000583 scm.RunCommand(command, self._options, args, file_list)
584 file_list = [os.path.join(name, f.strip()) for f in file_list]
585 self._options.revision = None
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000586
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000587 # Process the dependencies next (sort alphanumerically to ensure that
588 # containing directories get populated first and for readability)
maruel@chromium.org271375b2010-06-23 19:17:38 +0000589 deps = self._ParseAllDeps(entries)
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000590 deps_to_process = deps.keys()
591 deps_to_process.sort()
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000592
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000593 # First pass for direct dependencies.
594 if command == 'update' and not self._options.verbose:
595 pm = Progress('Syncing projects', len(deps_to_process))
596 for d in deps_to_process:
nasser@codeaurora.org1f7a3d12010-02-04 15:11:50 +0000597 if command == 'update' and not self._options.verbose:
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000598 pm.update()
599 if type(deps[d]) == str:
600 url = deps[d]
601 entries[d] = url
602 if run_scm:
603 self._options.revision = revision_overrides.get(d)
maruel@chromium.org75a59272010-06-11 22:34:03 +0000604 scm = gclient_scm.CreateSCM(url, self.root_dir(), d)
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000605 scm.RunCommand(command, self._options, args, file_list)
606 self._options.revision = None
607 elif isinstance(deps[d], self.FileImpl):
maruel@chromium.org491c04b2010-05-17 18:17:44 +0000608 file_dep = deps[d]
609 self._options.revision = file_dep.GetRevision()
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000610 if run_scm:
maruel@chromium.org75a59272010-06-11 22:34:03 +0000611 scm = gclient_scm.CreateSCM(file_dep.GetPath(), self.root_dir(), d)
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000612 scm.RunCommand("updatesingle", self._options,
maruel@chromium.org491c04b2010-05-17 18:17:44 +0000613 args + [file_dep.GetFilename()], file_list)
maruel@chromium.org79692d62010-05-14 18:57:13 +0000614
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000615 if command == 'update' and not self._options.verbose:
616 pm.end()
piman@chromium.org6f363722010-04-27 00:41:09 +0000617
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000618 # Second pass for inherited deps (via the From keyword)
619 for d in deps_to_process:
620 if isinstance(deps[d], self.FromImpl):
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000621 # Getting the URL from the sub_deps file can involve having to resolve
622 # a File() or having to resolve a relative URL. To resolve relative
623 # URLs, we need to pass in the orignal sub deps URL.
624 sub_deps_base_url = deps[deps[d].module_name]
maruel@chromium.org271375b2010-06-23 19:17:38 +0000625 sub_deps = Dependency(self, deps[d].module_name, sub_deps_base_url
626 ).ParseDepsFile(False)
maruel@chromium.org75a59272010-06-11 22:34:03 +0000627 url = deps[d].GetUrl(d, sub_deps_base_url, self.root_dir(), sub_deps)
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000628 entries[d] = url
629 if run_scm:
630 self._options.revision = revision_overrides.get(d)
maruel@chromium.org75a59272010-06-11 22:34:03 +0000631 scm = gclient_scm.CreateSCM(url, self.root_dir(), d)
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000632 scm.RunCommand(command, self._options, args, file_list)
633 self._options.revision = None
gspencer@google.comdf2d5902009-09-11 22:16:21 +0000634
phajdan.jr@chromium.orgd83b2b22009-08-11 15:30:55 +0000635 # Convert all absolute paths to relative.
636 for i in range(len(file_list)):
637 # TODO(phajdan.jr): We should know exactly when the paths are absolute.
638 # It depends on the command being executed (like runhooks vs sync).
639 if not os.path.isabs(file_list[i]):
640 continue
641
maruel@chromium.org75a59272010-06-11 22:34:03 +0000642 prefix = os.path.commonprefix([self.root_dir().lower(),
phajdan.jr@chromium.orgd83b2b22009-08-11 15:30:55 +0000643 file_list[i].lower()])
644 file_list[i] = file_list[i][len(prefix):]
645
646 # Strip any leading path separators.
647 while file_list[i].startswith('\\') or file_list[i].startswith('/'):
648 file_list[i] = file_list[i][1:]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000649
maruel@chromium.org75a59272010-06-11 22:34:03 +0000650 is_using_git = gclient_utils.IsUsingGit(self.root_dir(), entries.keys())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000651 self._RunHooks(command, file_list, is_using_git)
652
653 if command == 'update':
ajwong@chromium.orgcdcee802009-06-23 15:30:42 +0000654 # Notify the user if there is an orphaned entry in their working copy.
655 # Only delete the directory if there are no changes in it, and
656 # delete_unversioned_trees is set to true.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000657 prev_entries = self._ReadEntries()
658 for entry in prev_entries:
maruel@chromium.orgc5e9aec2009-08-03 18:25:56 +0000659 # Fix path separator on Windows.
660 entry_fixed = entry.replace('/', os.path.sep)
maruel@chromium.org75a59272010-06-11 22:34:03 +0000661 e_dir = os.path.join(self.root_dir(), entry_fixed)
maruel@chromium.orgc5e9aec2009-08-03 18:25:56 +0000662 # Use entry and not entry_fixed there.
maruel@chromium.org0329e672009-05-13 18:41:04 +0000663 if entry not in entries and os.path.exists(e_dir):
msb@chromium.org83017012009-09-28 18:52:12 +0000664 modified_files = False
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000665 if isinstance(prev_entries, list):
msb@chromium.org83017012009-09-28 18:52:12 +0000666 # old .gclient_entries format was list, now dict
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000667 modified_files = gclient_scm.scm.SVN.CaptureStatus(e_dir)
msb@chromium.org83017012009-09-28 18:52:12 +0000668 else:
669 file_list = []
maruel@chromium.org75a59272010-06-11 22:34:03 +0000670 scm = gclient_scm.CreateSCM(prev_entries[entry], self.root_dir(),
msb@chromium.org83017012009-09-28 18:52:12 +0000671 entry_fixed)
672 scm.status(self._options, [], file_list)
673 modified_files = file_list != []
674 if not self._options.delete_unversioned_trees or modified_files:
maruel@chromium.orgc5e9aec2009-08-03 18:25:56 +0000675 # There are modified files in this entry. Keep warning until
676 # removed.
maruel@chromium.orgc5e9aec2009-08-03 18:25:56 +0000677 print(("\nWARNING: \"%s\" is no longer part of this client. "
678 "It is recommended that you manually remove it.\n") %
679 entry_fixed)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000680 else:
681 # Delete the entry
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000682 print("\n________ deleting \'%s\' " +
maruel@chromium.org75a59272010-06-11 22:34:03 +0000683 "in \'%s\'") % (entry_fixed, self.root_dir())
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000684 gclient_utils.RemoveDirectory(e_dir)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000685 # record the current list of entries for next time
686 self._SaveEntries(entries)
maruel@chromium.org17cdf762010-05-28 17:30:52 +0000687 return 0
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000688
689 def PrintRevInfo(self):
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000690 """Output revision info mapping for the client and its dependencies.
691
692 This allows the capture of an overall "revision" for the source tree that
maruel@chromium.org918a9ae2010-05-28 15:50:30 +0000693 can be used to reproduce the same tree in the future. It is only useful for
694 "unpinned dependencies", i.e. DEPS/deps references without a svn revision
695 number or a git hash. A git branch name isn't "pinned" since the actual
696 commit can change.
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000697
698 The --snapshot option allows creating a .gclient file to reproduce the tree.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000699 """
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000700 if not self.dependencies:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000701 raise gclient_utils.Error("No solution specified")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000702
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000703 # Inner helper to generate base url and rev tuple
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000704 def GetURLAndRev(name, original_url):
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000705 url, _ = gclient_utils.SplitUrlRevision(original_url)
maruel@chromium.org75a59272010-06-11 22:34:03 +0000706 scm = gclient_scm.CreateSCM(original_url, self.root_dir(), name)
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000707 return (url, scm.revinfo(self._options, [], None))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000708
maruel@chromium.orge3da35f2010-03-09 21:40:45 +0000709 # text of the snapshot gclient file
710 new_gclient = ""
711 # Dictionary of { path : SCM url } to ensure no duplicate solutions
712 solution_names = {}
nasser@codeaurora.orgde8f3522010-03-11 23:47:44 +0000713 entries = {}
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000714 # Run on the base solutions first.
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000715 for solution in self.dependencies:
maruel@chromium.orge3da35f2010-03-09 21:40:45 +0000716 # Dictionary of { path : SCM url } to describe the gclient checkout
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000717 name = solution.name
maruel@chromium.orge3da35f2010-03-09 21:40:45 +0000718 if name in solution_names:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000719 raise gclient_utils.Error("solution %s specified more than once" % name)
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000720 (url, rev) = GetURLAndRev(name, solution.url)
msb@chromium.org770ff9e2009-09-23 17:18:18 +0000721 entries[name] = "%s@%s" % (url, rev)
maruel@chromium.orge3da35f2010-03-09 21:40:45 +0000722 solution_names[name] = "%s@%s" % (url, rev)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000723
nasser@codeaurora.orgde8f3522010-03-11 23:47:44 +0000724 # Process the dependencies next (sort alphanumerically to ensure that
725 # containing directories get populated first and for readability)
maruel@chromium.org271375b2010-06-23 19:17:38 +0000726 deps = self._ParseAllDeps(entries)
nasser@codeaurora.orgde8f3522010-03-11 23:47:44 +0000727 deps_to_process = deps.keys()
728 deps_to_process.sort()
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000729
nasser@codeaurora.orgde8f3522010-03-11 23:47:44 +0000730 # First pass for direct dependencies.
731 for d in deps_to_process:
732 if type(deps[d]) == str:
733 (url, rev) = GetURLAndRev(d, deps[d])
734 entries[d] = "%s@%s" % (url, rev)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000735
nasser@codeaurora.orgde8f3522010-03-11 23:47:44 +0000736 # Second pass for inherited deps (via the From keyword)
737 for d in deps_to_process:
tony@chromium.org4b5b1772010-04-08 01:52:56 +0000738 if isinstance(deps[d], self.FromImpl):
nasser@codeaurora.orgde8f3522010-03-11 23:47:44 +0000739 deps_parent_url = entries[deps[d].module_name]
740 if deps_parent_url.find("@") < 0:
741 raise gclient_utils.Error("From %s missing revisioned url" %
742 deps[d].module_name)
maruel@chromium.org271375b2010-06-23 19:17:38 +0000743 sub_deps_base_url = deps[deps[d].module_name]
744 sub_deps = Dependency(self, deps[d].module_name, sub_deps_base_url
745 ).ParseDepsFile(False)
746 url = deps[d].GetUrl(d, sub_deps_base_url, self.root_dir(), sub_deps)
747 (url, rev) = GetURLAndRev(d, url)
nasser@codeaurora.orgde8f3522010-03-11 23:47:44 +0000748 entries[d] = "%s@%s" % (url, rev)
maruel@chromium.orge3da35f2010-03-09 21:40:45 +0000749
nasser@codeaurora.orgde8f3522010-03-11 23:47:44 +0000750 # Build the snapshot configuration string
751 if self._options.snapshot:
752 url = entries.pop(name)
753 custom_deps = ",\n ".join(["\"%s\": \"%s\"" % (x, entries[x])
754 for x in sorted(entries.keys())])
maruel@chromium.orge3da35f2010-03-09 21:40:45 +0000755
maruel@chromium.org1f7d1182010-05-17 18:17:38 +0000756 new_gclient += self.DEFAULT_SNAPSHOT_SOLUTION_TEXT % {
nasser@codeaurora.orgde8f3522010-03-11 23:47:44 +0000757 'solution_name': name,
758 'solution_url': url,
759 'safesync_url' : "",
760 'solution_deps': custom_deps,
761 }
762 else:
763 print(";\n".join(["%s: %s" % (x, entries[x])
764 for x in sorted(entries.keys())]))
maruel@chromium.orge3da35f2010-03-09 21:40:45 +0000765
766 # Print the snapshot configuration file
767 if self._options.snapshot:
maruel@chromium.org491c04b2010-05-17 18:17:44 +0000768 config = self.DEFAULT_SNAPSHOT_FILE_TEXT % {'solution_list': new_gclient}
maruel@chromium.org75a59272010-06-11 22:34:03 +0000769 snapclient = GClient(self.root_dir(), self._options)
maruel@chromium.orge3da35f2010-03-09 21:40:45 +0000770 snapclient.SetConfig(config)
maruel@chromium.org116704f2010-06-11 17:34:38 +0000771 print(snapclient.config_content)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000772
maruel@chromium.org75a59272010-06-11 22:34:03 +0000773 def root_dir(self):
774 return self._root_dir
775
maruel@chromium.org271375b2010-06-23 19:17:38 +0000776 def enforced_os(self):
777 return self._enforced_os
778
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000779
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000780#### gclient commands.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000781
782
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000783def CMDcleanup(parser, args):
784 """Cleans up all working copies.
maruel@chromium.orgddff62d2010-05-17 21:02:36 +0000785
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000786Mostly svn-specific. Simply runs 'svn cleanup' for each module.
maruel@chromium.org79692d62010-05-14 18:57:13 +0000787"""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000788 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
789 help='override deps for the specified (comma-separated) '
790 'platform(s); \'all\' will process all deps_os '
791 'references')
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000792 (options, args) = parser.parse_args(args)
maruel@chromium.org2806acc2009-05-15 12:33:34 +0000793 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000794 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000795 raise gclient_utils.Error('client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000796 if options.verbose:
797 # Print out the .gclient file. This is longer than if we just printed the
798 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.org116704f2010-06-11 17:34:38 +0000799 print(client.config_content)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000800 return client.RunOnDeps('cleanup', args)
801
802
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000803@attr('usage', '[url] [safesync url]')
804def CMDconfig(parser, args):
maruel@chromium.orgddff62d2010-05-17 21:02:36 +0000805 """Create a .gclient file in the current directory.
806
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000807This specifies the configuration for further commands. After update/sync,
maruel@chromium.org79692d62010-05-14 18:57:13 +0000808top-level DEPS files in each module are read to determine dependent
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000809modules to operate on as well. If optional [url] parameter is
maruel@chromium.org79692d62010-05-14 18:57:13 +0000810provided, then configuration is read from a specified Subversion server
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000811URL.
maruel@chromium.org79692d62010-05-14 18:57:13 +0000812"""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000813 parser.add_option('--spec',
814 help='create a gclient file containing the provided '
815 'string. Due to Cygwin/Python brokenness, it '
816 'probably can\'t contain any newlines.')
817 parser.add_option('--name',
818 help='overrides the default name for the solution')
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000819 (options, args) = parser.parse_args(args)
maruel@chromium.org5fc2a332010-05-26 19:37:15 +0000820 if ((options.spec and args) or len(args) > 2 or
821 (not options.spec and not args)):
822 parser.error('Inconsistent arguments. Use either --spec or one or 2 args')
823
maruel@chromium.org0329e672009-05-13 18:41:04 +0000824 if os.path.exists(options.config_filename):
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000825 raise gclient_utils.Error('%s file already exists in the current directory'
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000826 % options.config_filename)
maruel@chromium.org2806acc2009-05-15 12:33:34 +0000827 client = GClient('.', options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000828 if options.spec:
829 client.SetConfig(options.spec)
830 else:
maruel@chromium.org1ab7ffc2009-06-03 17:21:37 +0000831 base_url = args[0].rstrip('/')
iposva@chromium.org8cf7a392010-04-07 17:20:26 +0000832 if not options.name:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000833 name = base_url.split('/')[-1]
iposva@chromium.org8cf7a392010-04-07 17:20:26 +0000834 else:
835 # specify an alternate relpath for the given URL.
836 name = options.name
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000837 safesync_url = ''
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000838 if len(args) > 1:
839 safesync_url = args[1]
840 client.SetDefaultConfig(name, base_url, safesync_url)
841 client.SaveConfig()
maruel@chromium.org79692d62010-05-14 18:57:13 +0000842 return 0
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000843
844
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000845def CMDexport(parser, args):
maruel@chromium.orgddff62d2010-05-17 21:02:36 +0000846 """Wrapper for svn export for all managed directories."""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000847 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
848 help='override deps for the specified (comma-separated) '
849 'platform(s); \'all\' will process all deps_os '
850 'references')
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000851 (options, args) = parser.parse_args(args)
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +0000852 if len(args) != 1:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000853 raise gclient_utils.Error('Need directory name')
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +0000854 client = GClient.LoadCurrentConfig(options)
855
856 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000857 raise gclient_utils.Error('client not configured; see \'gclient config\'')
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +0000858
859 if options.verbose:
860 # Print out the .gclient file. This is longer than if we just printed the
861 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.org116704f2010-06-11 17:34:38 +0000862 print(client.config_content)
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +0000863 return client.RunOnDeps('export', args)
864
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000865
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000866@attr('epilog', """Example:
867 gclient pack > patch.txt
868 generate simple patch for configured client and dependences
869""")
870def CMDpack(parser, args):
maruel@chromium.org79692d62010-05-14 18:57:13 +0000871 """Generate a patch which can be applied at the root of the tree.
maruel@chromium.orgddff62d2010-05-17 21:02:36 +0000872
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000873Internally, runs 'svn diff'/'git diff' on each checked out module and
maruel@chromium.org79692d62010-05-14 18:57:13 +0000874dependencies, and performs minimal postprocessing of the output. The
875resulting patch is printed to stdout and can be applied to a freshly
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000876checked out tree via 'patch -p0 < patchfile'.
maruel@chromium.org79692d62010-05-14 18:57:13 +0000877"""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000878 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
879 help='override deps for the specified (comma-separated) '
880 'platform(s); \'all\' will process all deps_os '
881 'references')
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000882 (options, args) = parser.parse_args(args)
kbr@google.comab318592009-09-04 00:54:55 +0000883 client = GClient.LoadCurrentConfig(options)
884 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000885 raise gclient_utils.Error('client not configured; see \'gclient config\'')
kbr@google.comab318592009-09-04 00:54:55 +0000886 if options.verbose:
887 # Print out the .gclient file. This is longer than if we just printed the
888 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.org116704f2010-06-11 17:34:38 +0000889 print(client.config_content)
kbr@google.comab318592009-09-04 00:54:55 +0000890 return client.RunOnDeps('pack', args)
891
892
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000893def CMDstatus(parser, args):
894 """Show modification status for every dependencies."""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000895 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
896 help='override deps for the specified (comma-separated) '
897 'platform(s); \'all\' will process all deps_os '
898 'references')
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000899 (options, args) = parser.parse_args(args)
maruel@chromium.org2806acc2009-05-15 12:33:34 +0000900 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000901 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000902 raise gclient_utils.Error('client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000903 if options.verbose:
904 # Print out the .gclient file. This is longer than if we just printed the
905 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.org116704f2010-06-11 17:34:38 +0000906 print(client.config_content)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000907 return client.RunOnDeps('status', args)
908
909
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000910@attr('epilog', """Examples:
maruel@chromium.org79692d62010-05-14 18:57:13 +0000911 gclient sync
912 update files from SCM according to current configuration,
913 *for modules which have changed since last update or sync*
914 gclient sync --force
915 update files from SCM according to current configuration, for
916 all modules (useful for recovering files deleted from local copy)
917 gclient sync --revision src@31000
918 update src directory to r31000
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000919""")
920def CMDsync(parser, args):
921 """Checkout/update all modules."""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000922 parser.add_option('-f', '--force', action='store_true',
923 help='force update even for unchanged modules')
924 parser.add_option('-n', '--nohooks', action='store_true',
925 help='don\'t run hooks after the update is complete')
926 parser.add_option('-r', '--revision', action='append',
927 dest='revisions', metavar='REV', default=[],
928 help='Enforces revision/hash for the solutions with the '
929 'format src@rev. The src@ part is optional and can be '
930 'skipped. -r can be used multiple times when .gclient '
931 'has multiple solutions configured and will work even '
932 'if the src@ part is skipped.')
933 parser.add_option('-H', '--head', action='store_true',
934 help='skips any safesync_urls specified in '
935 'configured solutions and sync to head instead')
936 parser.add_option('-D', '--delete_unversioned_trees', action='store_true',
937 help='delete any unexpected unversioned trees '
938 'that are in the checkout')
939 parser.add_option('-R', '--reset', action='store_true',
940 help='resets any local changes before updating (git only)')
941 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
942 help='override deps for the specified (comma-separated) '
943 'platform(s); \'all\' will process all deps_os '
944 'references')
945 parser.add_option('-m', '--manually_grab_svn_rev', action='store_true',
946 help='Skip svn up whenever possible by requesting '
947 'actual HEAD revision from the repository')
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000948 (options, args) = parser.parse_args(args)
maruel@chromium.org2806acc2009-05-15 12:33:34 +0000949 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000950
951 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000952 raise gclient_utils.Error('client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000953
maruel@chromium.org307d1792010-05-31 20:03:13 +0000954 if options.revisions and options.head:
955 # TODO(maruel): Make it a parser.error if it doesn't break any builder.
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000956 print('Warning: you cannot use both --head and --revision')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000957
958 if options.verbose:
959 # Print out the .gclient file. This is longer than if we just printed the
960 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.org116704f2010-06-11 17:34:38 +0000961 print(client.config_content)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000962 return client.RunOnDeps('update', args)
963
964
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000965def CMDupdate(parser, args):
maruel@chromium.orgddff62d2010-05-17 21:02:36 +0000966 """Alias for the sync command. Deprecated."""
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000967 return CMDsync(parser, args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000968
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000969def CMDdiff(parser, args):
970 """Displays local diff for every dependencies."""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000971 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
972 help='override deps for the specified (comma-separated) '
973 'platform(s); \'all\' will process all deps_os '
974 'references')
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000975 (options, args) = parser.parse_args(args)
maruel@chromium.org2806acc2009-05-15 12:33:34 +0000976 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000977 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000978 raise gclient_utils.Error('client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000979 if options.verbose:
980 # Print out the .gclient file. This is longer than if we just printed the
981 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.org116704f2010-06-11 17:34:38 +0000982 print(client.config_content)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000983 return client.RunOnDeps('diff', args)
984
985
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000986def CMDrevert(parser, args):
987 """Revert all modifications in every dependencies."""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000988 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
989 help='override deps for the specified (comma-separated) '
990 'platform(s); \'all\' will process all deps_os '
991 'references')
992 parser.add_option('-n', '--nohooks', action='store_true',
993 help='don\'t run hooks after the revert is complete')
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000994 (options, args) = parser.parse_args(args)
995 # --force is implied.
996 options.force = True
maruel@chromium.org2806acc2009-05-15 12:33:34 +0000997 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000998 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000999 raise gclient_utils.Error('client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001000 return client.RunOnDeps('revert', args)
1001
1002
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001003def CMDrunhooks(parser, args):
1004 """Runs hooks for files that have been modified in the local working copy."""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00001005 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
1006 help='override deps for the specified (comma-separated) '
1007 'platform(s); \'all\' will process all deps_os '
1008 'references')
1009 parser.add_option('-f', '--force', action='store_true', default=True,
1010 help='Deprecated. No effect.')
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001011 (options, args) = parser.parse_args(args)
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001012 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001013 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00001014 raise gclient_utils.Error('client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001015 if options.verbose:
1016 # Print out the .gclient file. This is longer than if we just printed the
1017 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.org116704f2010-06-11 17:34:38 +00001018 print(client.config_content)
maruel@chromium.org5df6a462009-08-28 18:52:26 +00001019 options.force = True
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001020 options.nohooks = False
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001021 return client.RunOnDeps('runhooks', args)
1022
1023
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001024def CMDrevinfo(parser, args):
maruel@chromium.org9eda4112010-06-11 18:56:10 +00001025 """Output revision info mapping for the client and its dependencies.
1026
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00001027 This allows the capture of an overall 'revision' for the source tree that
maruel@chromium.org9eda4112010-06-11 18:56:10 +00001028 can be used to reproduce the same tree in the future. It is only useful for
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00001029 'unpinned dependencies', i.e. DEPS/deps references without a svn revision
1030 number or a git hash. A git branch name isn't 'pinned' since the actual
maruel@chromium.org9eda4112010-06-11 18:56:10 +00001031 commit can change.
1032 """
1033 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
1034 help='override deps for the specified (comma-separated) '
1035 'platform(s); \'all\' will process all deps_os '
1036 'references')
1037 parser.add_option('-s', '--snapshot', action='store_true',
1038 help='creates a snapshot .gclient file of the current '
1039 'version of all repositories to reproduce the tree, '
1040 'implies -a')
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001041 (options, args) = parser.parse_args(args)
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001042 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001043 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00001044 raise gclient_utils.Error('client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001045 client.PrintRevInfo()
maruel@chromium.org79692d62010-05-14 18:57:13 +00001046 return 0
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001047
1048
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001049def Command(name):
1050 return getattr(sys.modules[__name__], 'CMD' + name, None)
1051
1052
1053def CMDhelp(parser, args):
1054 """Prints list of commands or help for a specific command."""
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001055 (_, args) = parser.parse_args(args)
maruel@chromium.orgddff62d2010-05-17 21:02:36 +00001056 if len(args) == 1:
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001057 return Main(args + ['--help'])
maruel@chromium.orgddff62d2010-05-17 21:02:36 +00001058 parser.print_help()
1059 return 0
1060
1061
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001062def GenUsage(parser, command):
1063 """Modify an OptParse object with the function's documentation."""
1064 obj = Command(command)
1065 if command == 'help':
1066 command = '<command>'
1067 # OptParser.description prefer nicely non-formatted strings.
1068 parser.description = re.sub('[\r\n ]{2,}', ' ', obj.__doc__)
1069 usage = getattr(obj, 'usage', '')
1070 parser.set_usage('%%prog %s [options] %s' % (command, usage))
1071 parser.epilog = getattr(obj, 'epilog', None)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001072
1073
1074def Main(argv):
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001075 """Doesn't parse the arguments here, just find the right subcommand to
1076 execute."""
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001077 try:
1078 # Do it late so all commands are listed.
1079 CMDhelp.usage = ('\n\nCommands are:\n' + '\n'.join([
1080 ' %-10s %s' % (fn[3:], Command(fn[3:]).__doc__.split('\n')[0].strip())
1081 for fn in dir(sys.modules[__name__]) if fn.startswith('CMD')]))
1082 parser = optparse.OptionParser(version='%prog ' + __version__)
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001083 parser.add_option('-v', '--verbose', action='count', default=0,
1084 help='Produces additional output for diagnostics. Can be '
1085 'used up to three times for more logging info.')
1086 parser.add_option('--gclientfile', dest='config_filename',
1087 default=os.environ.get('GCLIENT_FILE', '.gclient'),
1088 help='Specify an alternate %default file')
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001089 # Integrate standard options processing.
1090 old_parser = parser.parse_args
1091 def Parse(args):
1092 (options, args) = old_parser(args)
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001093 level = None
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001094 if options.verbose == 2:
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001095 level = logging.INFO
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001096 elif options.verbose > 2:
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001097 level = logging.DEBUG
1098 logging.basicConfig(level=level,
1099 format='%(module)s(%(lineno)d) %(funcName)s:%(message)s')
1100 options.entries_filename = options.config_filename + '_entries'
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001101 if not hasattr(options, 'revisions'):
1102 # GClient.RunOnDeps expects it even if not applicable.
1103 options.revisions = []
1104 if not hasattr(options, 'head'):
1105 options.head = None
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001106 if not hasattr(options, 'nohooks'):
1107 options.nohooks = True
1108 if not hasattr(options, 'deps_os'):
1109 options.deps_os = None
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001110 return (options, args)
1111 parser.parse_args = Parse
1112 # We don't want wordwrapping in epilog (usually examples)
1113 parser.format_epilog = lambda _: parser.epilog or ''
1114 if argv:
1115 command = Command(argv[0])
1116 if command:
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001117 # 'fix' the usage and the description now that we know the subcommand.
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001118 GenUsage(parser, argv[0])
1119 return command(parser, argv[1:])
1120 # Not a known command. Default to help.
1121 GenUsage(parser, 'help')
1122 return CMDhelp(parser, argv)
1123 except gclient_utils.Error, e:
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001124 print >> sys.stderr, 'Error: %s' % str(e)
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001125 return 1
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001126
1127
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001128if '__main__' == __name__:
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001129 sys.exit(Main(sys.argv[1:]))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001130
1131# vim: ts=2:sw=2:tw=80:et: