blob: bc912f312f7f2866e15808d7b51650ed46247331 [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.orgdc7445d2010-07-09 21:05:29 +000052__version__ = "0.4.1"
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000053
maruel@chromium.orgdc7445d2010-07-09 21:05:29 +000054import 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
piman@chromium.org4b90e3a2010-07-01 20:28:26 +000060import subprocess
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000061import sys
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000062import urlparse
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000063import urllib
64
maruel@chromium.orgada4c652009-12-03 15:32:01 +000065import breakpad
66
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000067import gclient_scm
68import gclient_utils
nasser@codeaurora.org1f7a3d12010-02-04 15:11:50 +000069from third_party.repo.progress import Progress
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000070
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000071
maruel@chromium.org1f7d1182010-05-17 18:17:38 +000072def attr(attr, data):
73 """Sets an attribute on a function."""
74 def hook(fn):
75 setattr(fn, attr, data)
76 return fn
77 return hook
maruel@chromium.orge3da35f2010-03-09 21:40:45 +000078
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000079
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000080## GClient implementation.
81
82
maruel@chromium.org116704f2010-06-11 17:34:38 +000083class GClientKeywords(object):
84 class FromImpl(object):
85 """Used to implement the From() syntax."""
86
87 def __init__(self, module_name, sub_target_name=None):
88 """module_name is the dep module we want to include from. It can also be
89 the name of a subdirectory to include from.
90
91 sub_target_name is an optional parameter if the module name in the other
92 DEPS file is different. E.g., you might want to map src/net to net."""
93 self.module_name = module_name
94 self.sub_target_name = sub_target_name
95
96 def __str__(self):
97 return 'From(%s, %s)' % (repr(self.module_name),
98 repr(self.sub_target_name))
99
maruel@chromium.orgdc7445d2010-07-09 21:05:29 +0000100 def GetUrl(self, target_name, sub_deps_base_url, root_dir, sub_deps):
101 """Resolve the URL for this From entry."""
102 sub_deps_target_name = target_name
103 if self.sub_target_name:
104 sub_deps_target_name = self.sub_target_name
105 url = sub_deps[sub_deps_target_name]
106 if url.startswith('/'):
107 # If it's a relative URL, we need to resolve the URL relative to the
108 # sub deps base URL.
109 if not isinstance(sub_deps_base_url, basestring):
110 sub_deps_base_url = sub_deps_base_url.GetPath()
111 scm = gclient_scm.CreateSCM(sub_deps_base_url, root_dir,
112 None)
113 url = scm.FullUrlForRelativeUrl(url)
114 return url
115
maruel@chromium.org116704f2010-06-11 17:34:38 +0000116 class FileImpl(object):
117 """Used to implement the File('') syntax which lets you sync a single file
maruel@chromium.orge3216c62010-07-08 03:31:43 +0000118 from a SVN repo."""
maruel@chromium.org116704f2010-06-11 17:34:38 +0000119
120 def __init__(self, file_location):
121 self.file_location = file_location
122
123 def __str__(self):
124 return 'File("%s")' % self.file_location
125
126 def GetPath(self):
127 return os.path.split(self.file_location)[0]
128
129 def GetFilename(self):
130 rev_tokens = self.file_location.split('@')
131 return os.path.split(rev_tokens[0])[1]
132
133 def GetRevision(self):
134 rev_tokens = self.file_location.split('@')
135 if len(rev_tokens) > 1:
136 return rev_tokens[1]
137 return None
138
139 class VarImpl(object):
140 def __init__(self, custom_vars, local_scope):
141 self._custom_vars = custom_vars
142 self._local_scope = local_scope
143
144 def Lookup(self, var_name):
145 """Implements the Var syntax."""
146 if var_name in self._custom_vars:
147 return self._custom_vars[var_name]
148 elif var_name in self._local_scope.get("vars", {}):
149 return self._local_scope["vars"][var_name]
150 raise gclient_utils.Error("Var is not defined: %s" % var_name)
151
152
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000153class Dependency(GClientKeywords):
154 """Object that represents a dependency checkout."""
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000155 DEPS_FILE = 'DEPS'
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000156
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000157 def __init__(self, parent, name, url, safesync_url=None, custom_deps=None,
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000158 custom_vars=None, deps_file=None):
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000159 GClientKeywords.__init__(self)
160 self.parent = parent
161 self.name = name
162 self.url = url
163 # These 2 are only set in .gclient and not in DEPS files.
164 self.safesync_url = safesync_url
165 self.custom_vars = custom_vars or {}
166 self.custom_deps = custom_deps or {}
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000167 self.deps_hooks = []
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000168 self.dependencies = []
169 self.deps_file = deps_file or self.DEPS_FILE
maruel@chromium.org271375b2010-06-23 19:17:38 +0000170 self.deps_parsed = False
171 self.direct_reference = False
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000172
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000173 # Sanity checks
174 if not self.name and self.parent:
175 raise gclient_utils.Error('Dependency without name')
176 if not isinstance(self.url,
177 (basestring, self.FromImpl, self.FileImpl, None.__class__)):
178 raise gclient_utils.Error('dependency url must be either a string, None, '
179 'File() or From() instead of %s' %
180 self.url.__class__.__name__)
181 if '/' in self.deps_file or '\\' in self.deps_file:
182 raise gclient_utils.Error('deps_file name must not be a path, just a '
183 'filename. %s' % self.deps_file)
184
maruel@chromium.org271375b2010-06-23 19:17:38 +0000185 def ParseDepsFile(self, direct_reference):
186 """Parses the DEPS file for this dependency."""
187 if direct_reference:
188 # Maybe it was referenced earlier by a From() keyword but it's now
189 # directly referenced.
190 self.direct_reference = direct_reference
191 self.deps_parsed = True
192 filepath = os.path.join(self.root_dir(), self.name, self.deps_file)
193 if not os.path.isfile(filepath):
maruel@chromium.orgdc7445d2010-07-09 21:05:29 +0000194 return {}
maruel@chromium.org271375b2010-06-23 19:17:38 +0000195 deps_content = gclient_utils.FileRead(filepath)
maruel@chromium.org0d425922010-06-21 19:22:24 +0000196
maruel@chromium.org271375b2010-06-23 19:17:38 +0000197 # Eval the content.
198 # One thing is unintuitive, vars= {} must happen before Var() use.
199 local_scope = {}
200 var = self.VarImpl(self.custom_vars, local_scope)
201 global_scope = {
202 'File': self.FileImpl,
203 'From': self.FromImpl,
204 'Var': var.Lookup,
205 'deps_os': {},
206 }
maruel@chromium.org5990f9d2010-07-07 18:02:58 +0000207 try:
208 exec(deps_content, global_scope, local_scope)
209 except SyntaxError, e:
210 gclient_utils.SyntaxErrorToError(filepath, e)
maruel@chromium.org271375b2010-06-23 19:17:38 +0000211 deps = local_scope.get('deps', {})
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000212 # load os specific dependencies if defined. these dependencies may
213 # override or extend the values defined by the 'deps' member.
maruel@chromium.org271375b2010-06-23 19:17:38 +0000214 if 'deps_os' in local_scope:
215 for deps_os_key in self.enforced_os():
216 os_deps = local_scope['deps_os'].get(deps_os_key, {})
217 if len(self.enforced_os()) > 1:
218 # Ignore any conflict when including deps for more than one
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000219 # platform, so we collect the broadest set of dependencies available.
220 # We may end up with the wrong revision of something for our
221 # platform, but this is the best we can do.
222 deps.update([x for x in os_deps.items() if not x[0] in deps])
223 else:
224 deps.update(os_deps)
225
maruel@chromium.org271375b2010-06-23 19:17:38 +0000226 self.deps_hooks.extend(local_scope.get('hooks', []))
227
228 # If a line is in custom_deps, but not in the solution, we want to append
229 # this line to the solution.
230 for d in self.custom_deps:
231 if d not in deps:
232 deps[d] = self.custom_deps[d]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000233
234 # If use_relative_paths is set in the DEPS file, regenerate
235 # the dictionary using paths relative to the directory containing
236 # the DEPS file.
maruel@chromium.org271375b2010-06-23 19:17:38 +0000237 use_relative_paths = local_scope.get('use_relative_paths', False)
238 if use_relative_paths:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000239 rel_deps = {}
240 for d, url in deps.items():
241 # normpath is required to allow DEPS to use .. in their
242 # dependency local path.
maruel@chromium.org271375b2010-06-23 19:17:38 +0000243 rel_deps[os.path.normpath(os.path.join(self.name, d))] = url
244 deps = rel_deps
maruel@chromium.orgdc7445d2010-07-09 21:05:29 +0000245 # TODO(maruel): Add these dependencies into self.dependencies.
246 return deps
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000247
maruel@chromium.orgdc7445d2010-07-09 21:05:29 +0000248 def _ParseAllDeps(self, solution_urls):
249 """Parse the complete list of dependencies for the client.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000250
maruel@chromium.orgdc7445d2010-07-09 21:05:29 +0000251 Args:
252 solution_urls: A dict mapping module names (as relative paths) to URLs
253 corresponding to the solutions specified by the client. This parameter
254 is passed as an optimization.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000255
maruel@chromium.orgdc7445d2010-07-09 21:05:29 +0000256 Returns:
257 A dict mapping module names (as relative paths) to URLs corresponding
258 to the entire set of dependencies to checkout for the given client.
259
260 Raises:
261 Error: If a dependency conflicts with another dependency or of a solution.
262 """
263 deps = {}
264 for solution in self.dependencies:
265 solution_deps = solution.ParseDepsFile(True)
266
267 # If a line is in custom_deps, but not in the solution, we want to append
268 # this line to the solution.
269 for d in solution.custom_deps:
270 if d not in solution_deps:
271 solution_deps[d] = solution.custom_deps[d]
272
273 for d in solution_deps:
274 if d in solution.custom_deps:
275 # Dependency is overriden.
276 url = solution.custom_deps[d]
277 if url is None:
maruel@chromium.org917aa912010-07-09 20:35:18 +0000278 continue
maruel@chromium.orgdc7445d2010-07-09 21:05:29 +0000279 else:
280 url = solution_deps[d]
281 # if we have a From reference dependent on another solution, then
282 # just skip the From reference. When we pull deps for the solution,
283 # we will take care of this dependency.
284 #
285 # If multiple solutions all have the same From reference, then we
286 # should only add one to our list of dependencies.
287 if isinstance(url, self.FromImpl):
288 if url.module_name in solution_urls:
289 # Already parsed.
290 continue
291 if d in deps and type(deps[d]) != str:
292 if url.module_name == deps[d].module_name:
293 continue
294 elif isinstance(url, str):
295 parsed_url = urlparse.urlparse(url)
296 scheme = parsed_url[0]
297 if not scheme:
298 # A relative url. Fetch the real base.
299 path = parsed_url[2]
300 if path[0] != "/":
301 raise gclient_utils.Error(
302 "relative DEPS entry \"%s\" must begin with a slash" % d)
303 # Create a scm just to query the full url.
304 scm = gclient_scm.CreateSCM(solution.url, self.root_dir(),
305 None)
306 url = scm.FullUrlForRelativeUrl(url)
307 if d in deps and deps[d] != url:
308 raise gclient_utils.Error(
309 "Solutions have conflicting versions of dependency \"%s\"" % d)
310 if d in solution_urls and solution_urls[d] != url:
311 raise gclient_utils.Error(
312 "Dependency \"%s\" conflicts with specified solution" % d)
313 # Grab the dependency.
314 deps[d] = url
315 return deps
maruel@chromium.org5248d9d2010-07-07 20:03:51 +0000316
maruel@chromium.orgdc7445d2010-07-09 21:05:29 +0000317 def _RunHooks(self, command, file_list, is_using_git):
318 """Evaluates all hooks, running actions as needed.
319 """
320 # Hooks only run for these command types.
321 if not command in ('update', 'revert', 'runhooks'):
322 return
maruel@chromium.org917aa912010-07-09 20:35:18 +0000323
maruel@chromium.orgdc7445d2010-07-09 21:05:29 +0000324 # Hooks only run when --nohooks is not specified
325 if self._options.nohooks:
326 return
maruel@chromium.org917aa912010-07-09 20:35:18 +0000327
maruel@chromium.orgdc7445d2010-07-09 21:05:29 +0000328 # Get any hooks from the .gclient file.
329 hooks = self.deps_hooks[:]
330 # Add any hooks found in DEPS files.
331 for d in self.dependencies:
332 hooks.extend(d.deps_hooks)
333
334 # If "--force" was specified, run all hooks regardless of what files have
335 # changed. If the user is using git, then we don't know what files have
336 # changed so we always run all hooks.
337 if self._options.force or is_using_git:
338 for hook_dict in hooks:
339 self._RunHookAction(hook_dict, [])
340 return
341
342 # Run hooks on the basis of whether the files from the gclient operation
343 # match each hook's pattern.
344 for hook_dict in hooks:
345 pattern = re.compile(hook_dict['pattern'])
346 matching_file_list = [f for f in file_list if pattern.search(f)]
347 if matching_file_list:
348 self._RunHookAction(hook_dict, matching_file_list)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000349
maruel@chromium.orgeaf61062010-07-07 18:42:39 +0000350 def _RunHookAction(self, hook_dict, matching_file_list):
351 """Runs the action from a single hook."""
352 logging.info(hook_dict)
353 logging.info(matching_file_list)
354 command = hook_dict['action'][:]
355 if command[0] == 'python':
356 # If the hook specified "python" as the first item, the action is a
357 # Python script. Run it by starting a new copy of the same
358 # interpreter.
359 command[0] = sys.executable
360
361 if '$matching_files' in command:
362 splice_index = command.index('$matching_files')
363 command[splice_index:splice_index + 1] = matching_file_list
364
365 # Use a discrete exit status code of 2 to indicate that a hook action
366 # failed. Users of this script may wish to treat hook action failures
367 # differently from VC failures.
368 return gclient_utils.SubprocessCall(command, self.root_dir(), fail_status=2)
369
maruel@chromium.org271375b2010-06-23 19:17:38 +0000370 def root_dir(self):
371 return self.parent.root_dir()
372
373 def enforced_os(self):
374 return self.parent.enforced_os()
375
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000376 def recursion_limit(self):
377 return self.parent.recursion_limit() - 1
378
379 def tree(self, force_all):
380 return self.parent.tree(force_all)
381
382 def get_custom_deps(self, name, url):
383 """Returns a custom deps if applicable."""
384 if self.parent:
385 url = self.parent.get_custom_deps(name, url)
386 # None is a valid return value to disable a dependency.
387 return self.custom_deps.get(name, url)
388
389 def __str__(self):
390 out = []
391 for i in ('name', 'url', 'safesync_url', 'custom_deps', 'custom_vars',
maruel@chromium.orgdc7445d2010-07-09 21:05:29 +0000392 'deps_hooks'):
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000393 # 'deps_file'
394 if self.__dict__[i]:
395 out.append('%s: %s' % (i, self.__dict__[i]))
396
397 for d in self.dependencies:
398 out.extend([' ' + x for x in str(d).splitlines()])
399 out.append('')
400 return '\n'.join(out)
401
402 def __repr__(self):
403 return '%s: %s' % (self.name, self.url)
404
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000405
406class GClient(Dependency):
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000407 """Object that represent a gclient checkout. A tree of Dependency(), one per
408 solution or DEPS entry."""
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000409
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 },
maruel@chromium.org73e21142010-07-05 13:32:01 +0000427 "safesync_url": "%(safesync_url)s",
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000428 },
429]
430""")
431
432 DEFAULT_SNAPSHOT_SOLUTION_TEXT = ("""\
433 { "name" : "%(solution_name)s",
434 "url" : "%(solution_url)s",
435 "custom_deps" : {
maruel@chromium.org73e21142010-07-05 13:32:01 +0000436%(solution_deps)s },
437 "safesync_url": "%(safesync_url)s",
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000438 },
439""")
440
441 DEFAULT_SNAPSHOT_FILE_TEXT = ("""\
442# Snapshot generated with gclient revinfo --snapshot
443solutions = [
maruel@chromium.org73e21142010-07-05 13:32:01 +0000444%(solution_list)s]
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000445""")
446
447 def __init__(self, root_dir, options):
448 Dependency.__init__(self, None, None, None)
maruel@chromium.org0d425922010-06-21 19:22:24 +0000449 self._options = options
maruel@chromium.org271375b2010-06-23 19:17:38 +0000450 if options.deps_os:
451 enforced_os = options.deps_os.split(',')
452 else:
453 enforced_os = [self.DEPS_OS_CHOICES.get(sys.platform, 'unix')]
454 if 'all' in enforced_os:
455 enforced_os = self.DEPS_OS_CHOICES.itervalues()
456 self._enforced_os = list(set(enforced_os))
457 self._root_dir = root_dir
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000458 self.config_content = None
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000459 # Do not change previous behavior. Only solution level and immediate DEPS
460 # are processed.
461 self._recursion_limit = 2
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000462
463 def SetConfig(self, content):
464 assert self.dependencies == []
465 config_dict = {}
466 self.config_content = content
467 try:
468 exec(content, config_dict)
469 except SyntaxError, e:
maruel@chromium.org5990f9d2010-07-07 18:02:58 +0000470 gclient_utils.SyntaxErrorToError('.gclient', e)
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000471 for s in config_dict.get('solutions', []):
maruel@chromium.org81843b82010-06-28 16:49:26 +0000472 try:
473 self.dependencies.append(Dependency(
474 self, s['name'], s['url'],
475 s.get('safesync_url', None),
476 s.get('custom_deps', {}),
477 s.get('custom_vars', {})))
478 except KeyError:
479 raise gclient_utils.Error('Invalid .gclient file. Solution is '
480 'incomplete: %s' % s)
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000481 # .gclient can have hooks.
482 self.deps_hooks = config_dict.get('hooks', [])
483
484 def SaveConfig(self):
485 gclient_utils.FileWrite(os.path.join(self.root_dir(),
486 self._options.config_filename),
487 self.config_content)
488
489 @staticmethod
490 def LoadCurrentConfig(options):
491 """Searches for and loads a .gclient file relative to the current working
492 dir. Returns a GClient object."""
493 path = gclient_utils.FindGclientRoot(os.getcwd(), options.config_filename)
494 if not path:
495 return None
496 client = GClient(path, options)
497 client.SetConfig(gclient_utils.FileRead(
498 os.path.join(path, options.config_filename)))
499 return client
500
501 def SetDefaultConfig(self, solution_name, solution_url, safesync_url):
502 self.SetConfig(self.DEFAULT_CLIENT_FILE_TEXT % {
503 'solution_name': solution_name,
504 'solution_url': solution_url,
505 'safesync_url' : safesync_url,
506 })
507
maruel@chromium.orgdc7445d2010-07-09 21:05:29 +0000508 def _SaveEntries(self, entries):
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000509 """Creates a .gclient_entries file to record the list of unique checkouts.
510
511 The .gclient_entries file lives in the same directory as .gclient.
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000512 """
513 # Sometimes pprint.pformat will use {', sometimes it'll use { ' ... It
514 # makes testing a bit too fun.
maruel@chromium.orgdc7445d2010-07-09 21:05:29 +0000515 result = pprint.pformat(entries, 2)
516 if result.startswith('{\''):
517 result = '{ \'' + result[2:]
518 text = 'entries = \\\n' + result + '\n'
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000519 file_path = os.path.join(self.root_dir(), self._options.entries_filename)
maruel@chromium.orgdc7445d2010-07-09 21:05:29 +0000520 gclient_utils.FileWrite(file_path, text)
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000521
522 def _ReadEntries(self):
523 """Read the .gclient_entries file for the given client.
524
525 Returns:
526 A sequence of solution names, which will be empty if there is the
527 entries file hasn't been created yet.
528 """
529 scope = {}
530 filename = os.path.join(self.root_dir(), self._options.entries_filename)
531 if not os.path.exists(filename):
maruel@chromium.org73e21142010-07-05 13:32:01 +0000532 return {}
maruel@chromium.org5990f9d2010-07-07 18:02:58 +0000533 try:
534 exec(gclient_utils.FileRead(filename), scope)
535 except SyntaxError, e:
536 gclient_utils.SyntaxErrorToError(filename, e)
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000537 return scope['entries']
538
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000539 def _EnforceRevisions(self):
maruel@chromium.org918a9ae2010-05-28 15:50:30 +0000540 """Checks for revision overrides."""
541 revision_overrides = {}
maruel@chromium.org307d1792010-05-31 20:03:13 +0000542 if self._options.head:
543 return revision_overrides
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000544 for s in self.dependencies:
545 if not s.safesync_url:
maruel@chromium.org307d1792010-05-31 20:03:13 +0000546 continue
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000547 handle = urllib.urlopen(s.safesync_url)
maruel@chromium.org307d1792010-05-31 20:03:13 +0000548 rev = handle.read().strip()
549 handle.close()
550 if len(rev):
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000551 self._options.revisions.append('%s@%s' % (s.name, rev))
maruel@chromium.org307d1792010-05-31 20:03:13 +0000552 if not self._options.revisions:
553 return revision_overrides
554 # --revision will take over safesync_url.
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000555 solutions_names = [s.name for s in self.dependencies]
maruel@chromium.org307d1792010-05-31 20:03:13 +0000556 index = 0
557 for revision in self._options.revisions:
558 if not '@' in revision:
559 # Support for --revision 123
560 revision = '%s@%s' % (solutions_names[index], revision)
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000561 sol, rev = revision.split('@', 1)
maruel@chromium.org307d1792010-05-31 20:03:13 +0000562 if not sol in solutions_names:
563 #raise gclient_utils.Error('%s is not a valid solution.' % sol)
564 print >> sys.stderr, ('Please fix your script, having invalid '
565 '--revision flags will soon considered an error.')
566 else:
maruel@chromium.org918a9ae2010-05-28 15:50:30 +0000567 revision_overrides[sol] = rev
maruel@chromium.org307d1792010-05-31 20:03:13 +0000568 index += 1
maruel@chromium.org918a9ae2010-05-28 15:50:30 +0000569 return revision_overrides
570
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000571 def RunOnDeps(self, command, args):
572 """Runs a command on each dependency in a client and its dependencies.
573
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000574 Args:
575 command: The command to use (e.g., 'status' or 'diff')
576 args: list of str - extra arguments to add to the command line.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000577 """
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000578 if not self.dependencies:
maruel@chromium.org73e21142010-07-05 13:32:01 +0000579 raise gclient_utils.Error('No solution specified')
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000580 revision_overrides = self._EnforceRevisions()
maruel@chromium.orgdc7445d2010-07-09 21:05:29 +0000581
582 # When running runhooks --force, there's no need to consult the SCM.
583 # All known hooks are expected to run unconditionally regardless of working
584 # copy state, so skip the SCM status check.
585 run_scm = not (command == 'runhooks' and self._options.force)
586
587 entries = {}
588 file_list = []
589 # Run on the base solutions first.
590 for solution in self.dependencies:
591 name = solution.name
592 if name in entries:
593 raise gclient_utils.Error("solution %s specified more than once" % name)
594 url = solution.url
595 entries[name] = url
596 if run_scm and url:
597 self._options.revision = revision_overrides.get(name)
598 scm = gclient_scm.CreateSCM(url, self.root_dir(), name)
599 scm.RunCommand(command, self._options, args, file_list)
600 file_list = [os.path.join(name, f.strip()) for f in file_list]
601 self._options.revision = None
602
603 # Process the dependencies next (sort alphanumerically to ensure that
604 # containing directories get populated first and for readability)
605 deps = self._ParseAllDeps(entries)
606 deps_to_process = deps.keys()
607 deps_to_process.sort()
608
609 # First pass for direct dependencies.
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000610 if command == 'update' and not self._options.verbose:
maruel@chromium.orgdc7445d2010-07-09 21:05:29 +0000611 pm = Progress('Syncing projects', len(deps_to_process))
612 for d in deps_to_process:
613 if command == 'update' and not self._options.verbose:
614 pm.update()
615 if type(deps[d]) == str:
616 url = deps[d]
617 entries[d] = url
618 if run_scm:
619 self._options.revision = revision_overrides.get(d)
620 scm = gclient_scm.CreateSCM(url, self.root_dir(), d)
621 scm.RunCommand(command, self._options, args, file_list)
622 self._options.revision = None
623 elif isinstance(deps[d], self.FileImpl):
624 if command in (None, 'cleanup', 'diff', 'pack', 'status'):
625 continue
626 file_dep = deps[d]
627 self._options.revision = file_dep.GetRevision()
628 if run_scm:
629 scm = gclient_scm.SVNWrapper(file_dep.GetPath(), self.root_dir(), d)
630 scm.RunCommand('updatesingle', self._options,
631 args + [file_dep.GetFilename()], file_list)
632
633 if command == 'update' and not self._options.verbose:
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000634 pm.end()
piman@chromium.org6f363722010-04-27 00:41:09 +0000635
maruel@chromium.orgdc7445d2010-07-09 21:05:29 +0000636 # Second pass for inherited deps (via the From keyword)
637 for d in deps_to_process:
638 if isinstance(deps[d], self.FromImpl):
639 # Getting the URL from the sub_deps file can involve having to resolve
640 # a File() or having to resolve a relative URL. To resolve relative
641 # URLs, we need to pass in the orignal sub deps URL.
642 sub_deps_base_url = deps[deps[d].module_name]
643 sub_deps = Dependency(self, deps[d].module_name, sub_deps_base_url
644 ).ParseDepsFile(False)
645 url = deps[d].GetUrl(d, sub_deps_base_url, self.root_dir(), sub_deps)
646 entries[d] = url
647 if run_scm:
648 self._options.revision = revision_overrides.get(d)
649 scm = gclient_scm.CreateSCM(url, self.root_dir(), d)
650 scm.RunCommand(command, self._options, args, file_list)
651 self._options.revision = None
652
653 # Convert all absolute paths to relative.
654 for i in range(len(file_list)):
655 # TODO(phajdan.jr): We should know exactly when the paths are absolute.
656 # It depends on the command being executed (like runhooks vs sync).
657 if not os.path.isabs(file_list[i]):
658 continue
659
660 prefix = os.path.commonprefix([self.root_dir().lower(),
661 file_list[i].lower()])
662 file_list[i] = file_list[i][len(prefix):]
663
664 # Strip any leading path separators.
665 while file_list[i].startswith('\\') or file_list[i].startswith('/'):
666 file_list[i] = file_list[i][1:]
667
668 is_using_git = gclient_utils.IsUsingGit(self.root_dir(), entries.keys())
669 self._RunHooks(command, file_list, is_using_git)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000670
671 if command == 'update':
ajwong@chromium.orgcdcee802009-06-23 15:30:42 +0000672 # Notify the user if there is an orphaned entry in their working copy.
673 # Only delete the directory if there are no changes in it, and
674 # delete_unversioned_trees is set to true.
maruel@chromium.orgdc7445d2010-07-09 21:05:29 +0000675 prev_entries = self._ReadEntries()
676 for entry in prev_entries:
maruel@chromium.orgc5e9aec2009-08-03 18:25:56 +0000677 # Fix path separator on Windows.
678 entry_fixed = entry.replace('/', os.path.sep)
maruel@chromium.org75a59272010-06-11 22:34:03 +0000679 e_dir = os.path.join(self.root_dir(), entry_fixed)
maruel@chromium.orgc5e9aec2009-08-03 18:25:56 +0000680 # Use entry and not entry_fixed there.
maruel@chromium.org0329e672009-05-13 18:41:04 +0000681 if entry not in entries and os.path.exists(e_dir):
maruel@chromium.orgdc7445d2010-07-09 21:05:29 +0000682 modified_files = False
683 if isinstance(prev_entries, list):
684 # old .gclient_entries format was list, now dict
685 modified_files = gclient_scm.scm.SVN.CaptureStatus(e_dir)
686 else:
687 file_list = []
688 scm = gclient_scm.CreateSCM(prev_entries[entry], self.root_dir(),
689 entry_fixed)
690 scm.status(self._options, [], file_list)
691 modified_files = file_list != []
msb@chromium.org83017012009-09-28 18:52:12 +0000692 if not self._options.delete_unversioned_trees or modified_files:
maruel@chromium.orgc5e9aec2009-08-03 18:25:56 +0000693 # There are modified files in this entry. Keep warning until
694 # removed.
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000695 print(('\nWARNING: \'%s\' is no longer part of this client. '
696 'It is recommended that you manually remove it.\n') %
maruel@chromium.orgc5e9aec2009-08-03 18:25:56 +0000697 entry_fixed)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000698 else:
699 # Delete the entry
maruel@chromium.org73e21142010-07-05 13:32:01 +0000700 print('\n________ deleting \'%s\' in \'%s\'' % (
701 entry_fixed, self.root_dir()))
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000702 gclient_utils.RemoveDirectory(e_dir)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000703 # record the current list of entries for next time
maruel@chromium.orgdc7445d2010-07-09 21:05:29 +0000704 self._SaveEntries(entries)
maruel@chromium.org17cdf762010-05-28 17:30:52 +0000705 return 0
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000706
707 def PrintRevInfo(self):
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000708 """Output revision info mapping for the client and its dependencies.
709
710 This allows the capture of an overall "revision" for the source tree that
maruel@chromium.org918a9ae2010-05-28 15:50:30 +0000711 can be used to reproduce the same tree in the future. It is only useful for
712 "unpinned dependencies", i.e. DEPS/deps references without a svn revision
713 number or a git hash. A git branch name isn't "pinned" since the actual
714 commit can change.
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000715
716 The --snapshot option allows creating a .gclient file to reproduce the tree.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000717 """
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000718 if not self.dependencies:
maruel@chromium.org73e21142010-07-05 13:32:01 +0000719 raise gclient_utils.Error('No solution specified')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000720
maruel@chromium.orgdc7445d2010-07-09 21:05:29 +0000721 # Inner helper to generate base url and rev tuple
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000722 def GetURLAndRev(name, original_url):
maruel@chromium.orgbaa578e2010-07-12 17:36:59 +0000723 if not original_url:
724 return None
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000725 url, _ = gclient_utils.SplitUrlRevision(original_url)
maruel@chromium.org75a59272010-06-11 22:34:03 +0000726 scm = gclient_scm.CreateSCM(original_url, self.root_dir(), name)
maruel@chromium.orgbaa578e2010-07-12 17:36:59 +0000727 return '%s@%s' % (url, scm.revinfo(self._options, [], None))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000728
maruel@chromium.orgdc7445d2010-07-09 21:05:29 +0000729 # text of the snapshot gclient file
730 new_gclient = ""
731 # Dictionary of { path : SCM url } to ensure no duplicate solutions
732 solution_names = {}
733 entries = {}
734 # Run on the base solutions first.
735 for solution in self.dependencies:
736 # Dictionary of { path : SCM url } to describe the gclient checkout
737 name = solution.name
738 if name in solution_names:
739 raise gclient_utils.Error("solution %s specified more than once" % name)
maruel@chromium.orgbaa578e2010-07-12 17:36:59 +0000740 url = GetURLAndRev(name, solution.url)
741 entries[name] = url
742 solution_names[name] = url
maruel@chromium.orgdc7445d2010-07-09 21:05:29 +0000743
744 # Process the dependencies next (sort alphanumerically to ensure that
745 # containing directories get populated first and for readability)
746 deps = self._ParseAllDeps(entries)
747 deps_to_process = deps.keys()
748 deps_to_process.sort()
749
750 # First pass for direct dependencies.
751 for d in deps_to_process:
752 if type(deps[d]) == str:
maruel@chromium.orgbaa578e2010-07-12 17:36:59 +0000753 entries[d] = GetURLAndRev(d, deps[d])
maruel@chromium.org9b133512010-07-12 17:29:52 +0000754 elif isinstance(deps[d], self.FileImpl):
maruel@chromium.orgbaa578e2010-07-12 17:36:59 +0000755 entries[d] = GetURLAndRev(d, deps[d].file_location)
maruel@chromium.orgdc7445d2010-07-09 21:05:29 +0000756
757 # Second pass for inherited deps (via the From keyword)
758 for d in deps_to_process:
759 if isinstance(deps[d], self.FromImpl):
760 deps_parent_url = entries[deps[d].module_name]
761 if deps_parent_url.find("@") < 0:
762 raise gclient_utils.Error("From %s missing revisioned url" %
763 deps[d].module_name)
764 sub_deps_base_url = deps[deps[d].module_name]
765 sub_deps = Dependency(self, deps[d].module_name, sub_deps_base_url
766 ).ParseDepsFile(False)
767 url = deps[d].GetUrl(d, sub_deps_base_url, self.root_dir(), sub_deps)
maruel@chromium.orgbaa578e2010-07-12 17:36:59 +0000768 entries[d] = GetURLAndRev(d, url)
769
770 # Build the snapshot configuration string
771 if self._options.snapshot:
772 url = entries.pop(name)
maruel@chromium.orgdc7445d2010-07-09 21:05:29 +0000773 custom_deps = ''.join([' \"%s\": \"%s\",\n' % (x, entries[x])
774 for x in sorted(entries.keys())])
775
776 new_gclient += self.DEFAULT_SNAPSHOT_SOLUTION_TEXT % {
777 'solution_name': name,
778 'solution_url': url,
779 'safesync_url' : '',
780 'solution_deps': custom_deps,
781 }
nasser@codeaurora.orgde8f3522010-03-11 23:47:44 +0000782 else:
maruel@chromium.orgdc7445d2010-07-09 21:05:29 +0000783 print(';\n'.join(['%s: %s' % (x, entries[x])
784 for x in sorted(entries.keys())]))
785
786 # Print the snapshot configuration file
787 if self._options.snapshot:
788 config = self.DEFAULT_SNAPSHOT_FILE_TEXT % {'solution_list': new_gclient}
789 snapclient = GClient(self.root_dir(), self._options)
790 snapclient.SetConfig(config)
791 print(snapclient.config_content)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000792
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000793 def ParseDepsFile(self, direct_reference):
794 """No DEPS to parse for a .gclient file."""
795 self.direct_reference = direct_reference
796 self.deps_parsed = True
797
maruel@chromium.org75a59272010-06-11 22:34:03 +0000798 def root_dir(self):
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000799 """Root directory of gclient checkout."""
maruel@chromium.org75a59272010-06-11 22:34:03 +0000800 return self._root_dir
801
maruel@chromium.org271375b2010-06-23 19:17:38 +0000802 def enforced_os(self):
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000803 """What deps_os entries that are to be parsed."""
maruel@chromium.org271375b2010-06-23 19:17:38 +0000804 return self._enforced_os
805
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000806 def recursion_limit(self):
807 """How recursive can each dependencies in DEPS file can load DEPS file."""
808 return self._recursion_limit
809
810 def tree(self, force_all):
811 """Returns a flat list of all the dependencies."""
812 def subtree(dep):
813 if not force_all and not dep.direct_reference:
814 # Was loaded from a From() keyword in a DEPS file, don't load all its
815 # dependencies.
816 return []
817 result = dep.dependencies[:]
818 for d in dep.dependencies:
819 result.extend(subtree(d))
820 return result
821 return subtree(self)
822
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000823
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000824#### gclient commands.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000825
826
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000827def CMDcleanup(parser, args):
828 """Cleans up all working copies.
maruel@chromium.orgddff62d2010-05-17 21:02:36 +0000829
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000830Mostly svn-specific. Simply runs 'svn cleanup' for each module.
maruel@chromium.org79692d62010-05-14 18:57:13 +0000831"""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000832 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
833 help='override deps for the specified (comma-separated) '
834 'platform(s); \'all\' will process all deps_os '
835 'references')
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000836 (options, args) = parser.parse_args(args)
maruel@chromium.org2806acc2009-05-15 12:33:34 +0000837 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000838 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000839 raise gclient_utils.Error('client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000840 if options.verbose:
841 # Print out the .gclient file. This is longer than if we just printed the
842 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.org116704f2010-06-11 17:34:38 +0000843 print(client.config_content)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000844 return client.RunOnDeps('cleanup', args)
845
846
piman@chromium.org4b90e3a2010-07-01 20:28:26 +0000847@attr('usage', '[command] [args ...]')
848def CMDrecurse(parser, args):
849 """Operates on all the entries.
850
851 Runs a shell command on all entries.
852 """
853 # Stop parsing at the first non-arg so that these go through to the command
854 parser.disable_interspersed_args()
855 parser.add_option('-s', '--scm', action='append', default=[],
856 help='choose scm types to operate upon')
857 options, args = parser.parse_args(args)
858 root, entries = gclient_utils.GetGClientRootAndEntries()
859 scm_set = set()
860 for scm in options.scm:
861 scm_set.update(scm.split(','))
862
863 # Pass in the SCM type as an env variable
864 env = os.environ.copy()
865
866 for path, url in entries.iteritems():
867 scm = gclient_scm.GetScmName(url)
868 if scm_set and scm not in scm_set:
869 continue
870 dir = os.path.normpath(os.path.join(root, path))
871 env['GCLIENT_SCM'] = scm
872 env['GCLIENT_URL'] = url
873 subprocess.Popen(args, cwd=dir, env=env).communicate()
874
875
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000876@attr('usage', '[url] [safesync url]')
877def CMDconfig(parser, args):
maruel@chromium.orgddff62d2010-05-17 21:02:36 +0000878 """Create a .gclient file in the current directory.
879
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000880This specifies the configuration for further commands. After update/sync,
maruel@chromium.org79692d62010-05-14 18:57:13 +0000881top-level DEPS files in each module are read to determine dependent
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000882modules to operate on as well. If optional [url] parameter is
maruel@chromium.org79692d62010-05-14 18:57:13 +0000883provided, then configuration is read from a specified Subversion server
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000884URL.
maruel@chromium.org79692d62010-05-14 18:57:13 +0000885"""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000886 parser.add_option('--spec',
887 help='create a gclient file containing the provided '
888 'string. Due to Cygwin/Python brokenness, it '
889 'probably can\'t contain any newlines.')
890 parser.add_option('--name',
891 help='overrides the default name for the solution')
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000892 (options, args) = parser.parse_args(args)
maruel@chromium.org5fc2a332010-05-26 19:37:15 +0000893 if ((options.spec and args) or len(args) > 2 or
894 (not options.spec and not args)):
895 parser.error('Inconsistent arguments. Use either --spec or one or 2 args')
896
maruel@chromium.org0329e672009-05-13 18:41:04 +0000897 if os.path.exists(options.config_filename):
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000898 raise gclient_utils.Error('%s file already exists in the current directory'
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000899 % options.config_filename)
maruel@chromium.org2806acc2009-05-15 12:33:34 +0000900 client = GClient('.', options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000901 if options.spec:
902 client.SetConfig(options.spec)
903 else:
maruel@chromium.org1ab7ffc2009-06-03 17:21:37 +0000904 base_url = args[0].rstrip('/')
iposva@chromium.org8cf7a392010-04-07 17:20:26 +0000905 if not options.name:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000906 name = base_url.split('/')[-1]
iposva@chromium.org8cf7a392010-04-07 17:20:26 +0000907 else:
908 # specify an alternate relpath for the given URL.
909 name = options.name
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000910 safesync_url = ''
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000911 if len(args) > 1:
912 safesync_url = args[1]
913 client.SetDefaultConfig(name, base_url, safesync_url)
914 client.SaveConfig()
maruel@chromium.org79692d62010-05-14 18:57:13 +0000915 return 0
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000916
917
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000918def CMDexport(parser, args):
maruel@chromium.orgddff62d2010-05-17 21:02:36 +0000919 """Wrapper for svn export for all managed directories."""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000920 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
921 help='override deps for the specified (comma-separated) '
922 'platform(s); \'all\' will process all deps_os '
923 'references')
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000924 (options, args) = parser.parse_args(args)
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +0000925 if len(args) != 1:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000926 raise gclient_utils.Error('Need directory name')
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +0000927 client = GClient.LoadCurrentConfig(options)
928
929 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000930 raise gclient_utils.Error('client not configured; see \'gclient config\'')
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +0000931
932 if options.verbose:
933 # Print out the .gclient file. This is longer than if we just printed the
934 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.org116704f2010-06-11 17:34:38 +0000935 print(client.config_content)
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +0000936 return client.RunOnDeps('export', args)
937
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000938
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000939@attr('epilog', """Example:
940 gclient pack > patch.txt
941 generate simple patch for configured client and dependences
942""")
943def CMDpack(parser, args):
maruel@chromium.org79692d62010-05-14 18:57:13 +0000944 """Generate a patch which can be applied at the root of the tree.
maruel@chromium.orgddff62d2010-05-17 21:02:36 +0000945
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000946Internally, runs 'svn diff'/'git diff' on each checked out module and
maruel@chromium.org79692d62010-05-14 18:57:13 +0000947dependencies, and performs minimal postprocessing of the output. The
948resulting patch is printed to stdout and can be applied to a freshly
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000949checked out tree via 'patch -p0 < patchfile'.
maruel@chromium.org79692d62010-05-14 18:57:13 +0000950"""
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)
kbr@google.comab318592009-09-04 00:54:55 +0000956 client = GClient.LoadCurrentConfig(options)
957 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000958 raise gclient_utils.Error('client not configured; see \'gclient config\'')
kbr@google.comab318592009-09-04 00:54:55 +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)
kbr@google.comab318592009-09-04 00:54:55 +0000963 return client.RunOnDeps('pack', args)
964
965
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000966def CMDstatus(parser, args):
967 """Show modification status for every dependencies."""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000968 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
969 help='override deps for the specified (comma-separated) '
970 'platform(s); \'all\' will process all deps_os '
971 'references')
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000972 (options, args) = parser.parse_args(args)
maruel@chromium.org2806acc2009-05-15 12:33:34 +0000973 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000974 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000975 raise gclient_utils.Error('client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000976 if options.verbose:
977 # Print out the .gclient file. This is longer than if we just printed the
978 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.org116704f2010-06-11 17:34:38 +0000979 print(client.config_content)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000980 return client.RunOnDeps('status', args)
981
982
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000983@attr('epilog', """Examples:
maruel@chromium.org79692d62010-05-14 18:57:13 +0000984 gclient sync
985 update files from SCM according to current configuration,
986 *for modules which have changed since last update or sync*
987 gclient sync --force
988 update files from SCM according to current configuration, for
989 all modules (useful for recovering files deleted from local copy)
990 gclient sync --revision src@31000
991 update src directory to r31000
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000992""")
993def CMDsync(parser, args):
994 """Checkout/update all modules."""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000995 parser.add_option('-f', '--force', action='store_true',
996 help='force update even for unchanged modules')
997 parser.add_option('-n', '--nohooks', action='store_true',
998 help='don\'t run hooks after the update is complete')
999 parser.add_option('-r', '--revision', action='append',
1000 dest='revisions', metavar='REV', default=[],
1001 help='Enforces revision/hash for the solutions with the '
1002 'format src@rev. The src@ part is optional and can be '
1003 'skipped. -r can be used multiple times when .gclient '
1004 'has multiple solutions configured and will work even '
1005 'if the src@ part is skipped.')
1006 parser.add_option('-H', '--head', action='store_true',
1007 help='skips any safesync_urls specified in '
1008 'configured solutions and sync to head instead')
1009 parser.add_option('-D', '--delete_unversioned_trees', action='store_true',
1010 help='delete any unexpected unversioned trees '
1011 'that are in the checkout')
1012 parser.add_option('-R', '--reset', action='store_true',
1013 help='resets any local changes before updating (git only)')
1014 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
1015 help='override deps for the specified (comma-separated) '
1016 'platform(s); \'all\' will process all deps_os '
1017 'references')
1018 parser.add_option('-m', '--manually_grab_svn_rev', action='store_true',
1019 help='Skip svn up whenever possible by requesting '
1020 'actual HEAD revision from the repository')
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001021 (options, args) = parser.parse_args(args)
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001022 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001023
1024 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00001025 raise gclient_utils.Error('client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001026
maruel@chromium.org307d1792010-05-31 20:03:13 +00001027 if options.revisions and options.head:
1028 # TODO(maruel): Make it a parser.error if it doesn't break any builder.
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00001029 print('Warning: you cannot use both --head and --revision')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001030
1031 if options.verbose:
1032 # Print out the .gclient file. This is longer than if we just printed the
1033 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.org116704f2010-06-11 17:34:38 +00001034 print(client.config_content)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001035 return client.RunOnDeps('update', args)
1036
1037
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001038def CMDupdate(parser, args):
maruel@chromium.orgddff62d2010-05-17 21:02:36 +00001039 """Alias for the sync command. Deprecated."""
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001040 return CMDsync(parser, args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001041
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001042def CMDdiff(parser, args):
1043 """Displays local diff for 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')
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001048 (options, args) = parser.parse_args(args)
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001049 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001050 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00001051 raise gclient_utils.Error('client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001052 if options.verbose:
1053 # Print out the .gclient file. This is longer than if we just printed the
1054 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.org116704f2010-06-11 17:34:38 +00001055 print(client.config_content)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001056 return client.RunOnDeps('diff', args)
1057
1058
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001059def CMDrevert(parser, args):
1060 """Revert all modifications in every dependencies."""
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('-n', '--nohooks', action='store_true',
1066 help='don\'t run hooks after the revert is complete')
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001067 (options, args) = parser.parse_args(args)
1068 # --force is implied.
1069 options.force = True
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001070 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001071 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00001072 raise gclient_utils.Error('client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001073 return client.RunOnDeps('revert', args)
1074
1075
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001076def CMDrunhooks(parser, args):
1077 """Runs hooks for files that have been modified in the local working copy."""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00001078 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
1079 help='override deps for the specified (comma-separated) '
1080 'platform(s); \'all\' will process all deps_os '
1081 'references')
1082 parser.add_option('-f', '--force', action='store_true', default=True,
1083 help='Deprecated. No effect.')
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001084 (options, args) = parser.parse_args(args)
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001085 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001086 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00001087 raise gclient_utils.Error('client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001088 if options.verbose:
1089 # Print out the .gclient file. This is longer than if we just printed the
1090 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.org116704f2010-06-11 17:34:38 +00001091 print(client.config_content)
maruel@chromium.org5df6a462009-08-28 18:52:26 +00001092 options.force = True
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001093 options.nohooks = False
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001094 return client.RunOnDeps('runhooks', args)
1095
1096
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001097def CMDrevinfo(parser, args):
maruel@chromium.org9eda4112010-06-11 18:56:10 +00001098 """Output revision info mapping for the client and its dependencies.
1099
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00001100 This allows the capture of an overall 'revision' for the source tree that
maruel@chromium.org9eda4112010-06-11 18:56:10 +00001101 can be used to reproduce the same tree in the future. It is only useful for
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00001102 'unpinned dependencies', i.e. DEPS/deps references without a svn revision
1103 number or a git hash. A git branch name isn't 'pinned' since the actual
maruel@chromium.org9eda4112010-06-11 18:56:10 +00001104 commit can change.
1105 """
1106 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
1107 help='override deps for the specified (comma-separated) '
1108 'platform(s); \'all\' will process all deps_os '
1109 'references')
1110 parser.add_option('-s', '--snapshot', action='store_true',
1111 help='creates a snapshot .gclient file of the current '
maruel@chromium.orgdc7445d2010-07-09 21:05:29 +00001112 'version of all repositories to reproduce the tree, '
1113 'implies -a')
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001114 (options, args) = parser.parse_args(args)
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001115 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001116 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00001117 raise gclient_utils.Error('client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001118 client.PrintRevInfo()
maruel@chromium.org79692d62010-05-14 18:57:13 +00001119 return 0
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001120
1121
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001122def Command(name):
1123 return getattr(sys.modules[__name__], 'CMD' + name, None)
1124
1125
1126def CMDhelp(parser, args):
1127 """Prints list of commands or help for a specific command."""
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001128 (_, args) = parser.parse_args(args)
maruel@chromium.orgddff62d2010-05-17 21:02:36 +00001129 if len(args) == 1:
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001130 return Main(args + ['--help'])
maruel@chromium.orgddff62d2010-05-17 21:02:36 +00001131 parser.print_help()
1132 return 0
1133
1134
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001135def GenUsage(parser, command):
1136 """Modify an OptParse object with the function's documentation."""
1137 obj = Command(command)
1138 if command == 'help':
1139 command = '<command>'
1140 # OptParser.description prefer nicely non-formatted strings.
1141 parser.description = re.sub('[\r\n ]{2,}', ' ', obj.__doc__)
1142 usage = getattr(obj, 'usage', '')
1143 parser.set_usage('%%prog %s [options] %s' % (command, usage))
1144 parser.epilog = getattr(obj, 'epilog', None)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001145
1146
1147def Main(argv):
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001148 """Doesn't parse the arguments here, just find the right subcommand to
1149 execute."""
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001150 try:
1151 # Do it late so all commands are listed.
1152 CMDhelp.usage = ('\n\nCommands are:\n' + '\n'.join([
1153 ' %-10s %s' % (fn[3:], Command(fn[3:]).__doc__.split('\n')[0].strip())
1154 for fn in dir(sys.modules[__name__]) if fn.startswith('CMD')]))
1155 parser = optparse.OptionParser(version='%prog ' + __version__)
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001156 parser.add_option('-v', '--verbose', action='count', default=0,
1157 help='Produces additional output for diagnostics. Can be '
1158 'used up to three times for more logging info.')
1159 parser.add_option('--gclientfile', dest='config_filename',
1160 default=os.environ.get('GCLIENT_FILE', '.gclient'),
1161 help='Specify an alternate %default file')
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001162 # Integrate standard options processing.
1163 old_parser = parser.parse_args
1164 def Parse(args):
1165 (options, args) = old_parser(args)
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001166 level = None
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001167 if options.verbose == 2:
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001168 level = logging.INFO
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001169 elif options.verbose > 2:
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001170 level = logging.DEBUG
1171 logging.basicConfig(level=level,
1172 format='%(module)s(%(lineno)d) %(funcName)s:%(message)s')
1173 options.entries_filename = options.config_filename + '_entries'
maruel@chromium.orge3216c62010-07-08 03:31:43 +00001174
1175 # These hacks need to die.
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001176 if not hasattr(options, 'revisions'):
1177 # GClient.RunOnDeps expects it even if not applicable.
1178 options.revisions = []
1179 if not hasattr(options, 'head'):
1180 options.head = None
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001181 if not hasattr(options, 'nohooks'):
1182 options.nohooks = True
1183 if not hasattr(options, 'deps_os'):
1184 options.deps_os = None
maruel@chromium.orge3216c62010-07-08 03:31:43 +00001185 if not hasattr(options, 'manually_grab_svn_rev'):
1186 options.manually_grab_svn_rev = None
1187 if not hasattr(options, 'force'):
1188 options.force = None
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001189 return (options, args)
1190 parser.parse_args = Parse
1191 # We don't want wordwrapping in epilog (usually examples)
1192 parser.format_epilog = lambda _: parser.epilog or ''
1193 if argv:
1194 command = Command(argv[0])
1195 if command:
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001196 # 'fix' the usage and the description now that we know the subcommand.
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001197 GenUsage(parser, argv[0])
1198 return command(parser, argv[1:])
1199 # Not a known command. Default to help.
1200 GenUsage(parser, 'help')
1201 return CMDhelp(parser, argv)
1202 except gclient_utils.Error, e:
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001203 print >> sys.stderr, 'Error: %s' % str(e)
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001204 return 1
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001205
1206
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001207if '__main__' == __name__:
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001208 sys.exit(Main(sys.argv[1:]))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001209
1210# vim: ts=2:sw=2:tw=80:et: