blob: 6ec2f183fe4a95f94b2f9110d9bfc3f1dd6535e7 [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.orgadecb312010-07-07 19:31:49 +000052__version__ = "0.5"
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000053
maruel@chromium.org754960e2009-09-21 12:31:05 +000054import logging
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000055import optparse
56import os
msb@chromium.org2e38de72009-09-28 17:04:47 +000057import pprint
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000058import re
piman@chromium.org4b90e3a2010-07-01 20:28:26 +000059import subprocess
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
maruel@chromium.org116704f2010-06-11 17:34:38 +000099 class FileImpl(object):
100 """Used to implement the File('') syntax which lets you sync a single file
101 from an SVN repo."""
102
103 def __init__(self, file_location):
104 self.file_location = file_location
105
106 def __str__(self):
107 return 'File("%s")' % self.file_location
108
109 def GetPath(self):
110 return os.path.split(self.file_location)[0]
111
112 def GetFilename(self):
113 rev_tokens = self.file_location.split('@')
114 return os.path.split(rev_tokens[0])[1]
115
116 def GetRevision(self):
117 rev_tokens = self.file_location.split('@')
118 if len(rev_tokens) > 1:
119 return rev_tokens[1]
120 return None
121
122 class VarImpl(object):
123 def __init__(self, custom_vars, local_scope):
124 self._custom_vars = custom_vars
125 self._local_scope = local_scope
126
127 def Lookup(self, var_name):
128 """Implements the Var syntax."""
129 if var_name in self._custom_vars:
130 return self._custom_vars[var_name]
131 elif var_name in self._local_scope.get("vars", {}):
132 return self._local_scope["vars"][var_name]
133 raise gclient_utils.Error("Var is not defined: %s" % var_name)
134
135
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000136class Dependency(GClientKeywords):
137 """Object that represents a dependency checkout."""
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000138 DEPS_FILE = 'DEPS'
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000139
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000140 def __init__(self, parent, name, url, safesync_url=None, custom_deps=None,
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000141 custom_vars=None, deps_file=None):
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000142 GClientKeywords.__init__(self)
143 self.parent = parent
144 self.name = name
145 self.url = url
maruel@chromium.orgadecb312010-07-07 19:31:49 +0000146 self.parsed_url = None
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000147 # These 2 are only set in .gclient and not in DEPS files.
148 self.safesync_url = safesync_url
149 self.custom_vars = custom_vars or {}
150 self.custom_deps = custom_deps or {}
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000151 self.deps_hooks = []
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000152 self.dependencies = []
153 self.deps_file = deps_file or self.DEPS_FILE
maruel@chromium.orgadecb312010-07-07 19:31:49 +0000154 # A cache of the files affected by the current operation, necessary for
155 # hooks.
156 self.file_list = []
maruel@chromium.org271375b2010-06-23 19:17:38 +0000157 self.deps_parsed = False
158 self.direct_reference = False
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000159
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000160 # Sanity checks
161 if not self.name and self.parent:
162 raise gclient_utils.Error('Dependency without name')
maruel@chromium.orgadecb312010-07-07 19:31:49 +0000163 if self.name in [d.name for d in self.tree(False)]:
164 raise gclient_utils.Error('Dependency %s specified more than once' %
165 self.name)
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000166 if not isinstance(self.url,
167 (basestring, self.FromImpl, self.FileImpl, None.__class__)):
168 raise gclient_utils.Error('dependency url must be either a string, None, '
169 'File() or From() instead of %s' %
170 self.url.__class__.__name__)
171 if '/' in self.deps_file or '\\' in self.deps_file:
172 raise gclient_utils.Error('deps_file name must not be a path, just a '
173 'filename. %s' % self.deps_file)
174
maruel@chromium.orgadecb312010-07-07 19:31:49 +0000175 def LateOverride(self, url):
176 overriden_url = self.get_custom_deps(self.name, url)
177 if overriden_url != url:
178 self.parsed_url = overriden_url
179 logging.debug('%s, %s was overriden to %s' % (self.name, url,
180 self.parsed_url))
181 elif isinstance(url, self.FromImpl):
182 ref = [dep for dep in self.tree(True) if url.module_name == dep.name]
183 if not len(ref) == 1:
184 raise Exception('Failed to find one reference to %s. %s' % (
185 url.module_name, ref))
186 ref = ref[0]
187 sub_target = url.sub_target_name or url
188 # Make sure the referenced dependency DEPS file is loaded and file the
189 # inner referenced dependency.
190 ref.ParseDepsFile(False)
191 found_dep = None
192 for d in ref.dependencies:
193 if d.name == sub_target:
194 found_dep = d
195 break
196 if not found_dep:
197 raise Exception('Couldn\'t find %s in %s, referenced by %s' % (
198 sub_target, ref.name, self.name))
199 # Call LateOverride() again.
200 self.parsed_url = found_dep.LateOverride(found_dep.url)
201 logging.debug('%s, %s to %s' % (self.name, url, self.parsed_url))
202 elif isinstance(url, basestring):
203 parsed_url = urlparse.urlparse(url)
204 if not parsed_url[0]:
205 # A relative url. Fetch the real base.
206 path = parsed_url[2]
207 if not path.startswith('/'):
208 raise gclient_utils.Error(
209 'relative DEPS entry \'%s\' must begin with a slash' % url)
210 # Create a scm just to query the full url.
211 scm = gclient_scm.CreateSCM(self.parent.parsed_url, self.root_dir(),
212 None)
213 self.parsed_url = scm.FullUrlForRelativeUrl(url)
214 else:
215 self.parsed_url = url
216 logging.debug('%s, %s -> %s' % (self.name, url, self.parsed_url))
217 elif isinstance(url, self.FileImpl):
218 self.parsed_url = url
219 logging.debug('%s, %s -> %s (File)' % (self.name, url, self.parsed_url))
220 return self.parsed_url
221
maruel@chromium.org271375b2010-06-23 19:17:38 +0000222 def ParseDepsFile(self, direct_reference):
223 """Parses the DEPS file for this dependency."""
224 if direct_reference:
225 # Maybe it was referenced earlier by a From() keyword but it's now
226 # directly referenced.
227 self.direct_reference = direct_reference
maruel@chromium.orgadecb312010-07-07 19:31:49 +0000228 if self.deps_parsed:
229 return
maruel@chromium.org271375b2010-06-23 19:17:38 +0000230 self.deps_parsed = True
231 filepath = os.path.join(self.root_dir(), self.name, self.deps_file)
232 if not os.path.isfile(filepath):
maruel@chromium.orgadecb312010-07-07 19:31:49 +0000233 return
maruel@chromium.org271375b2010-06-23 19:17:38 +0000234 deps_content = gclient_utils.FileRead(filepath)
maruel@chromium.org0d425922010-06-21 19:22:24 +0000235
maruel@chromium.org271375b2010-06-23 19:17:38 +0000236 # Eval the content.
237 # One thing is unintuitive, vars= {} must happen before Var() use.
238 local_scope = {}
239 var = self.VarImpl(self.custom_vars, local_scope)
240 global_scope = {
241 'File': self.FileImpl,
242 'From': self.FromImpl,
243 'Var': var.Lookup,
244 'deps_os': {},
245 }
maruel@chromium.org5990f9d2010-07-07 18:02:58 +0000246 try:
247 exec(deps_content, global_scope, local_scope)
248 except SyntaxError, e:
249 gclient_utils.SyntaxErrorToError(filepath, e)
maruel@chromium.org271375b2010-06-23 19:17:38 +0000250 deps = local_scope.get('deps', {})
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000251 # load os specific dependencies if defined. these dependencies may
252 # override or extend the values defined by the 'deps' member.
maruel@chromium.org271375b2010-06-23 19:17:38 +0000253 if 'deps_os' in local_scope:
254 for deps_os_key in self.enforced_os():
255 os_deps = local_scope['deps_os'].get(deps_os_key, {})
256 if len(self.enforced_os()) > 1:
257 # Ignore any conflict when including deps for more than one
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000258 # platform, so we collect the broadest set of dependencies available.
259 # We may end up with the wrong revision of something for our
260 # platform, but this is the best we can do.
261 deps.update([x for x in os_deps.items() if not x[0] in deps])
262 else:
263 deps.update(os_deps)
264
maruel@chromium.org271375b2010-06-23 19:17:38 +0000265 self.deps_hooks.extend(local_scope.get('hooks', []))
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 self.custom_deps:
270 if d not in deps:
271 deps[d] = self.custom_deps[d]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000272
273 # If use_relative_paths is set in the DEPS file, regenerate
274 # the dictionary using paths relative to the directory containing
275 # the DEPS file.
maruel@chromium.org271375b2010-06-23 19:17:38 +0000276 use_relative_paths = local_scope.get('use_relative_paths', False)
277 if use_relative_paths:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000278 rel_deps = {}
279 for d, url in deps.items():
280 # normpath is required to allow DEPS to use .. in their
281 # dependency local path.
maruel@chromium.org271375b2010-06-23 19:17:38 +0000282 rel_deps[os.path.normpath(os.path.join(self.name, d))] = url
283 deps = rel_deps
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000284
maruel@chromium.orgadecb312010-07-07 19:31:49 +0000285 # Convert the deps into real Dependency.
286 for name, url in deps.iteritems():
287 if name in [s.name for s in self.dependencies]:
288 raise
289 self.dependencies.append(Dependency(self, name, url))
290 # Sort by name.
291 self.dependencies.sort(key=lambda x: x.name)
292 logging.info('Loaded: %s' % str(self))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000293
maruel@chromium.orgadecb312010-07-07 19:31:49 +0000294 def RunCommandRecursively(self, options, revision_overrides,
295 command, args, pm):
296 """Runs 'command' before parsing the DEPS in case it's a initial checkout
297 or a revert."""
298 assert self.file_list == []
299 # When running runhooks, there's no need to consult the SCM.
300 # All known hooks are expected to run unconditionally regardless of working
301 # copy state, so skip the SCM status check.
302 run_scm = command not in ('runhooks', None)
303 self.LateOverride(self.url)
304 if run_scm and self.parsed_url:
305 if isinstance(self.parsed_url, self.FileImpl):
306 # Special support for single-file checkout.
307 options.revision = self.parsed_url.GetRevision()
308 scm = gclient_scm.CreateSCM(self.parsed_url.GetPath(), self.root_dir(),
309 self.name)
310 scm.RunCommand("updatesingle", options,
311 args + [self.parsed_url.GetFilename()], self.file_list)
312 else:
313 options.revision = revision_overrides.get(self.name)
314 scm = gclient_scm.CreateSCM(self.parsed_url, self.root_dir(), self.name)
315 scm.RunCommand(command, options, args, self.file_list)
316 self.file_list = [os.path.join(self.name, f.strip())
317 for f in self.file_list]
318 options.revision = None
319 if pm:
320 # The + 1 comes from the fact that .gclient is considered a step in
321 # itself, .i.e. this code is called one time for the .gclient. This is not
322 # conceptually correct but it simplifies code.
323 pm._total = len(self.tree(False)) + 1
324 pm.update()
325 if self.recursion_limit():
326 # Then we can parse the DEPS file.
327 self.ParseDepsFile(True)
328 if pm:
329 pm._total = len(self.tree(False)) + 1
330 pm.update(0)
331 # Parse the dependencies of this dependency.
332 for s in self.dependencies:
333 # TODO(maruel): All these can run concurrently! No need for threads,
334 # just buffer stdout&stderr on pipes and flush as they complete.
335 # Watch out for stdin.
336 s.RunCommandRecursively(options, revision_overrides, command, args, pm)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000337
maruel@chromium.orgadecb312010-07-07 19:31:49 +0000338 def RunHooksRecursively(self, options):
339 """Evaluates all hooks, running actions as needed. RunCommandRecursively()
340 must have been called before to load the DEPS."""
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000341 # If "--force" was specified, run all hooks regardless of what files have
maruel@chromium.orgadecb312010-07-07 19:31:49 +0000342 # changed.
343 if self.deps_hooks:
344 # TODO(maruel): If the user is using git or git-svn, then we don't know
345 # what files have changed so we always run all hooks. It'd be nice to fix
346 # that.
347 if (options.force or
348 gclient_scm.GetScmName(self.parsed_url) in ('git', None) or
349 os.path.isdir(os.path.join(self.root_dir(), self.name, '.git'))):
350 for hook_dict in self.deps_hooks:
351 self._RunHookAction(hook_dict, [])
352 else:
353 # TODO(phajdan.jr): We should know exactly when the paths are absolute.
354 # Convert all absolute paths to relative.
355 for i in range(len(self.file_list)):
356 # It depends on the command being executed (like runhooks vs sync).
357 if not os.path.isabs(self.file_list[i]):
358 continue
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000359
maruel@chromium.orgadecb312010-07-07 19:31:49 +0000360 prefix = os.path.commonprefix([self.root_dir().lower(),
361 self.file_list[i].lower()])
362 self.file_list[i] = self.file_list[i][len(prefix):]
363
364 # Strip any leading path separators.
365 while file_list[i].startswith('\\') or file_list[i].startswith('/'):
366 self.file_list[i] = self.file_list[i][1:]
367
368 # Run hooks on the basis of whether the files from the gclient operation
369 # match each hook's pattern.
370 for hook_dict in self.deps_hooks:
371 pattern = re.compile(hook_dict['pattern'])
372 matching_file_list = [f for f in self.file_list if pattern.search(f)]
373 if matching_file_list:
374 self._RunHookAction(hook_dict, matching_file_list)
375 if self.recursion_limit():
376 for s in self.dependencies:
377 s.RunHooksRecursively(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000378
maruel@chromium.orgeaf61062010-07-07 18:42:39 +0000379 def _RunHookAction(self, hook_dict, matching_file_list):
380 """Runs the action from a single hook."""
381 logging.info(hook_dict)
382 logging.info(matching_file_list)
383 command = hook_dict['action'][:]
384 if command[0] == 'python':
385 # If the hook specified "python" as the first item, the action is a
386 # Python script. Run it by starting a new copy of the same
387 # interpreter.
388 command[0] = sys.executable
389
390 if '$matching_files' in command:
391 splice_index = command.index('$matching_files')
392 command[splice_index:splice_index + 1] = matching_file_list
393
394 # Use a discrete exit status code of 2 to indicate that a hook action
395 # failed. Users of this script may wish to treat hook action failures
396 # differently from VC failures.
397 return gclient_utils.SubprocessCall(command, self.root_dir(), fail_status=2)
398
maruel@chromium.org271375b2010-06-23 19:17:38 +0000399 def root_dir(self):
400 return self.parent.root_dir()
401
402 def enforced_os(self):
403 return self.parent.enforced_os()
404
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000405 def recursion_limit(self):
406 return self.parent.recursion_limit() - 1
407
408 def tree(self, force_all):
409 return self.parent.tree(force_all)
410
411 def get_custom_deps(self, name, url):
412 """Returns a custom deps if applicable."""
413 if self.parent:
414 url = self.parent.get_custom_deps(name, url)
415 # None is a valid return value to disable a dependency.
416 return self.custom_deps.get(name, url)
417
418 def __str__(self):
419 out = []
420 for i in ('name', 'url', 'safesync_url', 'custom_deps', 'custom_vars',
maruel@chromium.orgadecb312010-07-07 19:31:49 +0000421 'deps_hooks', 'file_list'):
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000422 # 'deps_file'
423 if self.__dict__[i]:
424 out.append('%s: %s' % (i, self.__dict__[i]))
425
426 for d in self.dependencies:
427 out.extend([' ' + x for x in str(d).splitlines()])
428 out.append('')
429 return '\n'.join(out)
430
431 def __repr__(self):
432 return '%s: %s' % (self.name, self.url)
433
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000434
435class GClient(Dependency):
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000436 """Object that represent a gclient checkout. A tree of Dependency(), one per
437 solution or DEPS entry."""
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000438
439 DEPS_OS_CHOICES = {
440 "win32": "win",
441 "win": "win",
442 "cygwin": "win",
443 "darwin": "mac",
444 "mac": "mac",
445 "unix": "unix",
446 "linux": "unix",
447 "linux2": "unix",
448 }
449
450 DEFAULT_CLIENT_FILE_TEXT = ("""\
451solutions = [
452 { "name" : "%(solution_name)s",
453 "url" : "%(solution_url)s",
454 "custom_deps" : {
455 },
maruel@chromium.org73e21142010-07-05 13:32:01 +0000456 "safesync_url": "%(safesync_url)s",
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000457 },
458]
459""")
460
461 DEFAULT_SNAPSHOT_SOLUTION_TEXT = ("""\
462 { "name" : "%(solution_name)s",
463 "url" : "%(solution_url)s",
464 "custom_deps" : {
maruel@chromium.org73e21142010-07-05 13:32:01 +0000465%(solution_deps)s },
466 "safesync_url": "%(safesync_url)s",
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000467 },
468""")
469
470 DEFAULT_SNAPSHOT_FILE_TEXT = ("""\
471# Snapshot generated with gclient revinfo --snapshot
472solutions = [
maruel@chromium.org73e21142010-07-05 13:32:01 +0000473%(solution_list)s]
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000474""")
475
476 def __init__(self, root_dir, options):
477 Dependency.__init__(self, None, None, None)
maruel@chromium.org0d425922010-06-21 19:22:24 +0000478 self._options = options
maruel@chromium.org271375b2010-06-23 19:17:38 +0000479 if options.deps_os:
480 enforced_os = options.deps_os.split(',')
481 else:
482 enforced_os = [self.DEPS_OS_CHOICES.get(sys.platform, 'unix')]
483 if 'all' in enforced_os:
484 enforced_os = self.DEPS_OS_CHOICES.itervalues()
485 self._enforced_os = list(set(enforced_os))
486 self._root_dir = root_dir
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000487 self.config_content = None
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000488 # Do not change previous behavior. Only solution level and immediate DEPS
489 # are processed.
490 self._recursion_limit = 2
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000491
492 def SetConfig(self, content):
493 assert self.dependencies == []
494 config_dict = {}
495 self.config_content = content
496 try:
497 exec(content, config_dict)
498 except SyntaxError, e:
maruel@chromium.org5990f9d2010-07-07 18:02:58 +0000499 gclient_utils.SyntaxErrorToError('.gclient', e)
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000500 for s in config_dict.get('solutions', []):
maruel@chromium.org81843b82010-06-28 16:49:26 +0000501 try:
502 self.dependencies.append(Dependency(
503 self, s['name'], s['url'],
504 s.get('safesync_url', None),
505 s.get('custom_deps', {}),
506 s.get('custom_vars', {})))
507 except KeyError:
508 raise gclient_utils.Error('Invalid .gclient file. Solution is '
509 'incomplete: %s' % s)
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000510 # .gclient can have hooks.
511 self.deps_hooks = config_dict.get('hooks', [])
512
513 def SaveConfig(self):
514 gclient_utils.FileWrite(os.path.join(self.root_dir(),
515 self._options.config_filename),
516 self.config_content)
517
518 @staticmethod
519 def LoadCurrentConfig(options):
520 """Searches for and loads a .gclient file relative to the current working
521 dir. Returns a GClient object."""
522 path = gclient_utils.FindGclientRoot(os.getcwd(), options.config_filename)
523 if not path:
524 return None
525 client = GClient(path, options)
526 client.SetConfig(gclient_utils.FileRead(
527 os.path.join(path, options.config_filename)))
528 return client
529
530 def SetDefaultConfig(self, solution_name, solution_url, safesync_url):
531 self.SetConfig(self.DEFAULT_CLIENT_FILE_TEXT % {
532 'solution_name': solution_name,
533 'solution_url': solution_url,
534 'safesync_url' : safesync_url,
535 })
536
maruel@chromium.orgadecb312010-07-07 19:31:49 +0000537 def _SaveEntries(self):
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000538 """Creates a .gclient_entries file to record the list of unique checkouts.
539
540 The .gclient_entries file lives in the same directory as .gclient.
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000541 """
542 # Sometimes pprint.pformat will use {', sometimes it'll use { ' ... It
543 # makes testing a bit too fun.
maruel@chromium.orgadecb312010-07-07 19:31:49 +0000544 result = 'entries = {\n'
545 for entry in self.tree(False):
546 result += ' %s: %s,\n' % (pprint.pformat(entry.name),
547 pprint.pformat(entry.parsed_url))
548 result += '}\n'
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000549 file_path = os.path.join(self.root_dir(), self._options.entries_filename)
maruel@chromium.orgadecb312010-07-07 19:31:49 +0000550 logging.info(result)
551 gclient_utils.FileWrite(file_path, result)
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000552
553 def _ReadEntries(self):
554 """Read the .gclient_entries file for the given client.
555
556 Returns:
557 A sequence of solution names, which will be empty if there is the
558 entries file hasn't been created yet.
559 """
560 scope = {}
561 filename = os.path.join(self.root_dir(), self._options.entries_filename)
562 if not os.path.exists(filename):
maruel@chromium.org73e21142010-07-05 13:32:01 +0000563 return {}
maruel@chromium.org5990f9d2010-07-07 18:02:58 +0000564 try:
565 exec(gclient_utils.FileRead(filename), scope)
566 except SyntaxError, e:
567 gclient_utils.SyntaxErrorToError(filename, e)
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000568 return scope['entries']
569
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000570 def _EnforceRevisions(self):
maruel@chromium.org918a9ae2010-05-28 15:50:30 +0000571 """Checks for revision overrides."""
572 revision_overrides = {}
maruel@chromium.org307d1792010-05-31 20:03:13 +0000573 if self._options.head:
574 return revision_overrides
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000575 for s in self.dependencies:
576 if not s.safesync_url:
maruel@chromium.org307d1792010-05-31 20:03:13 +0000577 continue
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000578 handle = urllib.urlopen(s.safesync_url)
maruel@chromium.org307d1792010-05-31 20:03:13 +0000579 rev = handle.read().strip()
580 handle.close()
581 if len(rev):
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000582 self._options.revisions.append('%s@%s' % (s.name, rev))
maruel@chromium.org307d1792010-05-31 20:03:13 +0000583 if not self._options.revisions:
584 return revision_overrides
585 # --revision will take over safesync_url.
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000586 solutions_names = [s.name for s in self.dependencies]
maruel@chromium.org307d1792010-05-31 20:03:13 +0000587 index = 0
588 for revision in self._options.revisions:
589 if not '@' in revision:
590 # Support for --revision 123
591 revision = '%s@%s' % (solutions_names[index], revision)
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000592 sol, rev = revision.split('@', 1)
maruel@chromium.org307d1792010-05-31 20:03:13 +0000593 if not sol in solutions_names:
594 #raise gclient_utils.Error('%s is not a valid solution.' % sol)
595 print >> sys.stderr, ('Please fix your script, having invalid '
596 '--revision flags will soon considered an error.')
597 else:
maruel@chromium.org918a9ae2010-05-28 15:50:30 +0000598 revision_overrides[sol] = rev
maruel@chromium.org307d1792010-05-31 20:03:13 +0000599 index += 1
maruel@chromium.org918a9ae2010-05-28 15:50:30 +0000600 return revision_overrides
601
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000602 def RunOnDeps(self, command, args):
603 """Runs a command on each dependency in a client and its dependencies.
604
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000605 Args:
606 command: The command to use (e.g., 'status' or 'diff')
607 args: list of str - extra arguments to add to the command line.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000608 """
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000609 if not self.dependencies:
maruel@chromium.org73e21142010-07-05 13:32:01 +0000610 raise gclient_utils.Error('No solution specified')
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000611 revision_overrides = self._EnforceRevisions()
maruel@chromium.orgadecb312010-07-07 19:31:49 +0000612 pm = None
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000613 if command == 'update' and not self._options.verbose:
maruel@chromium.orgadecb312010-07-07 19:31:49 +0000614 pm = Progress('Syncing projects', len(self.tree(False)) + 1)
615 self.RunCommandRecursively(self._options, revision_overrides,
616 command, args, pm)
617 if pm:
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000618 pm.end()
piman@chromium.org6f363722010-04-27 00:41:09 +0000619
maruel@chromium.orgadecb312010-07-07 19:31:49 +0000620 # Once all the dependencies have been processed, it's now safe to run the
621 # hooks.
622 if not self._options.nohooks:
623 self.RunHooksRecursively(self._options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000624
625 if command == 'update':
ajwong@chromium.orgcdcee802009-06-23 15:30:42 +0000626 # Notify the user if there is an orphaned entry in their working copy.
627 # Only delete the directory if there are no changes in it, and
628 # delete_unversioned_trees is set to true.
maruel@chromium.orgadecb312010-07-07 19:31:49 +0000629 entries = [i.name for i in self.tree(False)]
630 for entry, prev_url in self._ReadEntries().iteritems():
maruel@chromium.orgc5e9aec2009-08-03 18:25:56 +0000631 # Fix path separator on Windows.
632 entry_fixed = entry.replace('/', os.path.sep)
maruel@chromium.org75a59272010-06-11 22:34:03 +0000633 e_dir = os.path.join(self.root_dir(), entry_fixed)
maruel@chromium.orgc5e9aec2009-08-03 18:25:56 +0000634 # Use entry and not entry_fixed there.
maruel@chromium.org0329e672009-05-13 18:41:04 +0000635 if entry not in entries and os.path.exists(e_dir):
maruel@chromium.orgadecb312010-07-07 19:31:49 +0000636 file_list = []
637 scm = gclient_scm.CreateSCM(prev_url, self.root_dir(), entry_fixed)
638 scm.status(self._options, [], file_list)
639 modified_files = file_list != []
msb@chromium.org83017012009-09-28 18:52:12 +0000640 if not self._options.delete_unversioned_trees or modified_files:
maruel@chromium.orgc5e9aec2009-08-03 18:25:56 +0000641 # There are modified files in this entry. Keep warning until
642 # removed.
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000643 print(('\nWARNING: \'%s\' is no longer part of this client. '
644 'It is recommended that you manually remove it.\n') %
maruel@chromium.orgc5e9aec2009-08-03 18:25:56 +0000645 entry_fixed)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000646 else:
647 # Delete the entry
maruel@chromium.org73e21142010-07-05 13:32:01 +0000648 print('\n________ deleting \'%s\' in \'%s\'' % (
649 entry_fixed, self.root_dir()))
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000650 gclient_utils.RemoveDirectory(e_dir)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000651 # record the current list of entries for next time
maruel@chromium.orgadecb312010-07-07 19:31:49 +0000652 self._SaveEntries()
maruel@chromium.org17cdf762010-05-28 17:30:52 +0000653 return 0
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000654
655 def PrintRevInfo(self):
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000656 """Output revision info mapping for the client and its dependencies.
657
658 This allows the capture of an overall "revision" for the source tree that
maruel@chromium.org918a9ae2010-05-28 15:50:30 +0000659 can be used to reproduce the same tree in the future. It is only useful for
660 "unpinned dependencies", i.e. DEPS/deps references without a svn revision
661 number or a git hash. A git branch name isn't "pinned" since the actual
662 commit can change.
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000663
664 The --snapshot option allows creating a .gclient file to reproduce the tree.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000665 """
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000666 if not self.dependencies:
maruel@chromium.org73e21142010-07-05 13:32:01 +0000667 raise gclient_utils.Error('No solution specified')
maruel@chromium.orgadecb312010-07-07 19:31:49 +0000668 # Load all the settings.
669 self.RunCommandRecursively(self._options, {}, None, [], None)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000670
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000671 def GetURLAndRev(name, original_url):
maruel@chromium.orgadecb312010-07-07 19:31:49 +0000672 """Returns the revision-qualified SCM url."""
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000673 url, _ = gclient_utils.SplitUrlRevision(original_url)
maruel@chromium.org75a59272010-06-11 22:34:03 +0000674 scm = gclient_scm.CreateSCM(original_url, self.root_dir(), name)
maruel@chromium.orgadecb312010-07-07 19:31:49 +0000675 if not os.path.isdir(scm.checkout_path):
676 return None
677 return '%s@%s' % (url, scm.revinfo(self._options, [], None))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000678
nasser@codeaurora.orgde8f3522010-03-11 23:47:44 +0000679 if self._options.snapshot:
maruel@chromium.orgadecb312010-07-07 19:31:49 +0000680 new_gclient = ''
681 # First level at .gclient
682 for d in self.dependencies:
683 entries = {}
684 def GrabDeps(sol):
685 """Recursively grab dependencies."""
686 for i in sol.dependencies:
687 entries[i.name] = GetURLAndRev(i.name, i.parsed_url)
688 GrabDeps(i)
689 GrabDeps(d)
690 custom_deps = []
691 for k in sorted(entries.keys()):
692 if entries[k]:
693 # Quotes aren't escaped...
694 custom_deps.append(' \"%s\": \'%s\',\n' % (k, entries[k]))
695 else:
696 custom_deps.append(' \"%s\": None,\n' % k)
697 new_gclient += self.DEFAULT_SNAPSHOT_SOLUTION_TEXT % {
698 'solution_name': d.name,
699 'solution_url': d.url,
700 'safesync_url' : d.safesync_url or '',
701 'solution_deps': ''.join(custom_deps),
702 }
703 # Print the snapshot configuration file
704 print(self.DEFAULT_SNAPSHOT_FILE_TEXT % {'solution_list': new_gclient})
nasser@codeaurora.orgde8f3522010-03-11 23:47:44 +0000705 else:
maruel@chromium.orgadecb312010-07-07 19:31:49 +0000706 entries = sorted(self.tree(False), key=lambda i: i.name)
707 for entry in entries:
708 revision = GetURLAndRev(entry.name, entry.parsed_url)
709 line = '%s: %s' % (entry.name, revision)
710 if not entry is entries[-1]:
711 line += ';'
712 print line
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000713
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000714 def ParseDepsFile(self, direct_reference):
715 """No DEPS to parse for a .gclient file."""
716 self.direct_reference = direct_reference
717 self.deps_parsed = True
718
maruel@chromium.org75a59272010-06-11 22:34:03 +0000719 def root_dir(self):
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000720 """Root directory of gclient checkout."""
maruel@chromium.org75a59272010-06-11 22:34:03 +0000721 return self._root_dir
722
maruel@chromium.org271375b2010-06-23 19:17:38 +0000723 def enforced_os(self):
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000724 """What deps_os entries that are to be parsed."""
maruel@chromium.org271375b2010-06-23 19:17:38 +0000725 return self._enforced_os
726
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000727 def recursion_limit(self):
728 """How recursive can each dependencies in DEPS file can load DEPS file."""
729 return self._recursion_limit
730
731 def tree(self, force_all):
732 """Returns a flat list of all the dependencies."""
733 def subtree(dep):
734 if not force_all and not dep.direct_reference:
735 # Was loaded from a From() keyword in a DEPS file, don't load all its
736 # dependencies.
737 return []
738 result = dep.dependencies[:]
739 for d in dep.dependencies:
740 result.extend(subtree(d))
741 return result
742 return subtree(self)
743
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000744
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000745#### gclient commands.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000746
747
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000748def CMDcleanup(parser, args):
749 """Cleans up all working copies.
maruel@chromium.orgddff62d2010-05-17 21:02:36 +0000750
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000751Mostly svn-specific. Simply runs 'svn cleanup' for each module.
maruel@chromium.org79692d62010-05-14 18:57:13 +0000752"""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000753 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
754 help='override deps for the specified (comma-separated) '
755 'platform(s); \'all\' will process all deps_os '
756 'references')
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000757 (options, args) = parser.parse_args(args)
maruel@chromium.org2806acc2009-05-15 12:33:34 +0000758 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000759 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000760 raise gclient_utils.Error('client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000761 if options.verbose:
762 # Print out the .gclient file. This is longer than if we just printed the
763 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.org116704f2010-06-11 17:34:38 +0000764 print(client.config_content)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000765 return client.RunOnDeps('cleanup', args)
766
767
piman@chromium.org4b90e3a2010-07-01 20:28:26 +0000768@attr('usage', '[command] [args ...]')
769def CMDrecurse(parser, args):
770 """Operates on all the entries.
771
772 Runs a shell command on all entries.
773 """
774 # Stop parsing at the first non-arg so that these go through to the command
775 parser.disable_interspersed_args()
776 parser.add_option('-s', '--scm', action='append', default=[],
777 help='choose scm types to operate upon')
778 options, args = parser.parse_args(args)
779 root, entries = gclient_utils.GetGClientRootAndEntries()
780 scm_set = set()
781 for scm in options.scm:
782 scm_set.update(scm.split(','))
783
784 # Pass in the SCM type as an env variable
785 env = os.environ.copy()
786
787 for path, url in entries.iteritems():
788 scm = gclient_scm.GetScmName(url)
789 if scm_set and scm not in scm_set:
790 continue
791 dir = os.path.normpath(os.path.join(root, path))
792 env['GCLIENT_SCM'] = scm
793 env['GCLIENT_URL'] = url
794 subprocess.Popen(args, cwd=dir, env=env).communicate()
795
796
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000797@attr('usage', '[url] [safesync url]')
798def CMDconfig(parser, args):
maruel@chromium.orgddff62d2010-05-17 21:02:36 +0000799 """Create a .gclient file in the current directory.
800
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000801This specifies the configuration for further commands. After update/sync,
maruel@chromium.org79692d62010-05-14 18:57:13 +0000802top-level DEPS files in each module are read to determine dependent
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000803modules to operate on as well. If optional [url] parameter is
maruel@chromium.org79692d62010-05-14 18:57:13 +0000804provided, then configuration is read from a specified Subversion server
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000805URL.
maruel@chromium.org79692d62010-05-14 18:57:13 +0000806"""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000807 parser.add_option('--spec',
808 help='create a gclient file containing the provided '
809 'string. Due to Cygwin/Python brokenness, it '
810 'probably can\'t contain any newlines.')
811 parser.add_option('--name',
812 help='overrides the default name for the solution')
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000813 (options, args) = parser.parse_args(args)
maruel@chromium.org5fc2a332010-05-26 19:37:15 +0000814 if ((options.spec and args) or len(args) > 2 or
815 (not options.spec and not args)):
816 parser.error('Inconsistent arguments. Use either --spec or one or 2 args')
817
maruel@chromium.org0329e672009-05-13 18:41:04 +0000818 if os.path.exists(options.config_filename):
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000819 raise gclient_utils.Error('%s file already exists in the current directory'
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000820 % options.config_filename)
maruel@chromium.org2806acc2009-05-15 12:33:34 +0000821 client = GClient('.', options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000822 if options.spec:
823 client.SetConfig(options.spec)
824 else:
maruel@chromium.org1ab7ffc2009-06-03 17:21:37 +0000825 base_url = args[0].rstrip('/')
iposva@chromium.org8cf7a392010-04-07 17:20:26 +0000826 if not options.name:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000827 name = base_url.split('/')[-1]
iposva@chromium.org8cf7a392010-04-07 17:20:26 +0000828 else:
829 # specify an alternate relpath for the given URL.
830 name = options.name
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000831 safesync_url = ''
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000832 if len(args) > 1:
833 safesync_url = args[1]
834 client.SetDefaultConfig(name, base_url, safesync_url)
835 client.SaveConfig()
maruel@chromium.org79692d62010-05-14 18:57:13 +0000836 return 0
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000837
838
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000839def CMDexport(parser, args):
maruel@chromium.orgddff62d2010-05-17 21:02:36 +0000840 """Wrapper for svn export for all managed directories."""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000841 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
842 help='override deps for the specified (comma-separated) '
843 'platform(s); \'all\' will process all deps_os '
844 'references')
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000845 (options, args) = parser.parse_args(args)
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +0000846 if len(args) != 1:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000847 raise gclient_utils.Error('Need directory name')
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +0000848 client = GClient.LoadCurrentConfig(options)
849
850 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000851 raise gclient_utils.Error('client not configured; see \'gclient config\'')
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +0000852
853 if options.verbose:
854 # Print out the .gclient file. This is longer than if we just printed the
855 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.org116704f2010-06-11 17:34:38 +0000856 print(client.config_content)
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +0000857 return client.RunOnDeps('export', args)
858
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000859
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000860@attr('epilog', """Example:
861 gclient pack > patch.txt
862 generate simple patch for configured client and dependences
863""")
864def CMDpack(parser, args):
maruel@chromium.org79692d62010-05-14 18:57:13 +0000865 """Generate a patch which can be applied at the root of the tree.
maruel@chromium.orgddff62d2010-05-17 21:02:36 +0000866
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000867Internally, runs 'svn diff'/'git diff' on each checked out module and
maruel@chromium.org79692d62010-05-14 18:57:13 +0000868dependencies, and performs minimal postprocessing of the output. The
869resulting patch is printed to stdout and can be applied to a freshly
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000870checked out tree via 'patch -p0 < patchfile'.
maruel@chromium.org79692d62010-05-14 18:57:13 +0000871"""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000872 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
873 help='override deps for the specified (comma-separated) '
874 'platform(s); \'all\' will process all deps_os '
875 'references')
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000876 (options, args) = parser.parse_args(args)
kbr@google.comab318592009-09-04 00:54:55 +0000877 client = GClient.LoadCurrentConfig(options)
878 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000879 raise gclient_utils.Error('client not configured; see \'gclient config\'')
kbr@google.comab318592009-09-04 00:54:55 +0000880 if options.verbose:
881 # Print out the .gclient file. This is longer than if we just printed the
882 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.org116704f2010-06-11 17:34:38 +0000883 print(client.config_content)
kbr@google.comab318592009-09-04 00:54:55 +0000884 return client.RunOnDeps('pack', args)
885
886
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000887def CMDstatus(parser, args):
888 """Show modification status for every dependencies."""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000889 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
890 help='override deps for the specified (comma-separated) '
891 'platform(s); \'all\' will process all deps_os '
892 'references')
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000893 (options, args) = parser.parse_args(args)
maruel@chromium.org2806acc2009-05-15 12:33:34 +0000894 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000895 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000896 raise gclient_utils.Error('client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000897 if options.verbose:
898 # Print out the .gclient file. This is longer than if we just printed the
899 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.org116704f2010-06-11 17:34:38 +0000900 print(client.config_content)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000901 return client.RunOnDeps('status', args)
902
903
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000904@attr('epilog', """Examples:
maruel@chromium.org79692d62010-05-14 18:57:13 +0000905 gclient sync
906 update files from SCM according to current configuration,
907 *for modules which have changed since last update or sync*
908 gclient sync --force
909 update files from SCM according to current configuration, for
910 all modules (useful for recovering files deleted from local copy)
911 gclient sync --revision src@31000
912 update src directory to r31000
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000913""")
914def CMDsync(parser, args):
915 """Checkout/update all modules."""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000916 parser.add_option('-f', '--force', action='store_true',
917 help='force update even for unchanged modules')
918 parser.add_option('-n', '--nohooks', action='store_true',
919 help='don\'t run hooks after the update is complete')
920 parser.add_option('-r', '--revision', action='append',
921 dest='revisions', metavar='REV', default=[],
922 help='Enforces revision/hash for the solutions with the '
923 'format src@rev. The src@ part is optional and can be '
924 'skipped. -r can be used multiple times when .gclient '
925 'has multiple solutions configured and will work even '
926 'if the src@ part is skipped.')
927 parser.add_option('-H', '--head', action='store_true',
928 help='skips any safesync_urls specified in '
929 'configured solutions and sync to head instead')
930 parser.add_option('-D', '--delete_unversioned_trees', action='store_true',
931 help='delete any unexpected unversioned trees '
932 'that are in the checkout')
933 parser.add_option('-R', '--reset', action='store_true',
934 help='resets any local changes before updating (git only)')
935 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
936 help='override deps for the specified (comma-separated) '
937 'platform(s); \'all\' will process all deps_os '
938 'references')
939 parser.add_option('-m', '--manually_grab_svn_rev', action='store_true',
940 help='Skip svn up whenever possible by requesting '
941 'actual HEAD revision from the repository')
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000942 (options, args) = parser.parse_args(args)
maruel@chromium.org2806acc2009-05-15 12:33:34 +0000943 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000944
945 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000946 raise gclient_utils.Error('client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000947
maruel@chromium.org307d1792010-05-31 20:03:13 +0000948 if options.revisions and options.head:
949 # TODO(maruel): Make it a parser.error if it doesn't break any builder.
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000950 print('Warning: you cannot use both --head and --revision')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000951
952 if options.verbose:
953 # Print out the .gclient file. This is longer than if we just printed the
954 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.org116704f2010-06-11 17:34:38 +0000955 print(client.config_content)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000956 return client.RunOnDeps('update', args)
957
958
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000959def CMDupdate(parser, args):
maruel@chromium.orgddff62d2010-05-17 21:02:36 +0000960 """Alias for the sync command. Deprecated."""
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000961 return CMDsync(parser, args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000962
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000963def CMDdiff(parser, args):
964 """Displays local diff for every dependencies."""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000965 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
966 help='override deps for the specified (comma-separated) '
967 'platform(s); \'all\' will process all deps_os '
968 'references')
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000969 (options, args) = parser.parse_args(args)
maruel@chromium.org2806acc2009-05-15 12:33:34 +0000970 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000971 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000972 raise gclient_utils.Error('client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000973 if options.verbose:
974 # Print out the .gclient file. This is longer than if we just printed the
975 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.org116704f2010-06-11 17:34:38 +0000976 print(client.config_content)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000977 return client.RunOnDeps('diff', args)
978
979
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000980def CMDrevert(parser, args):
981 """Revert all modifications in every dependencies."""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000982 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
983 help='override deps for the specified (comma-separated) '
984 'platform(s); \'all\' will process all deps_os '
985 'references')
986 parser.add_option('-n', '--nohooks', action='store_true',
987 help='don\'t run hooks after the revert is complete')
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000988 (options, args) = parser.parse_args(args)
989 # --force is implied.
990 options.force = True
maruel@chromium.org2806acc2009-05-15 12:33:34 +0000991 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000992 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000993 raise gclient_utils.Error('client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000994 return client.RunOnDeps('revert', args)
995
996
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000997def CMDrunhooks(parser, args):
998 """Runs hooks for files that have been modified in the local working copy."""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000999 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
1000 help='override deps for the specified (comma-separated) '
1001 'platform(s); \'all\' will process all deps_os '
1002 'references')
1003 parser.add_option('-f', '--force', action='store_true', default=True,
1004 help='Deprecated. No effect.')
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001005 (options, args) = parser.parse_args(args)
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001006 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001007 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00001008 raise gclient_utils.Error('client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001009 if options.verbose:
1010 # Print out the .gclient file. This is longer than if we just printed the
1011 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.org116704f2010-06-11 17:34:38 +00001012 print(client.config_content)
maruel@chromium.org5df6a462009-08-28 18:52:26 +00001013 options.force = True
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001014 options.nohooks = False
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001015 return client.RunOnDeps('runhooks', args)
1016
1017
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001018def CMDrevinfo(parser, args):
maruel@chromium.org9eda4112010-06-11 18:56:10 +00001019 """Output revision info mapping for the client and its dependencies.
1020
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00001021 This allows the capture of an overall 'revision' for the source tree that
maruel@chromium.org9eda4112010-06-11 18:56:10 +00001022 can be used to reproduce the same tree in the future. It is only useful for
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00001023 'unpinned dependencies', i.e. DEPS/deps references without a svn revision
1024 number or a git hash. A git branch name isn't 'pinned' since the actual
maruel@chromium.org9eda4112010-06-11 18:56:10 +00001025 commit can change.
1026 """
1027 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
1028 help='override deps for the specified (comma-separated) '
1029 'platform(s); \'all\' will process all deps_os '
1030 'references')
1031 parser.add_option('-s', '--snapshot', action='store_true',
1032 help='creates a snapshot .gclient file of the current '
maruel@chromium.orgadecb312010-07-07 19:31:49 +00001033 'version of all repositories to reproduce the tree')
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001034 (options, args) = parser.parse_args(args)
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001035 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001036 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00001037 raise gclient_utils.Error('client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001038 client.PrintRevInfo()
maruel@chromium.org79692d62010-05-14 18:57:13 +00001039 return 0
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001040
1041
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001042def Command(name):
1043 return getattr(sys.modules[__name__], 'CMD' + name, None)
1044
1045
1046def CMDhelp(parser, args):
1047 """Prints list of commands or help for a specific command."""
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001048 (_, args) = parser.parse_args(args)
maruel@chromium.orgddff62d2010-05-17 21:02:36 +00001049 if len(args) == 1:
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001050 return Main(args + ['--help'])
maruel@chromium.orgddff62d2010-05-17 21:02:36 +00001051 parser.print_help()
1052 return 0
1053
1054
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001055def GenUsage(parser, command):
1056 """Modify an OptParse object with the function's documentation."""
1057 obj = Command(command)
1058 if command == 'help':
1059 command = '<command>'
1060 # OptParser.description prefer nicely non-formatted strings.
1061 parser.description = re.sub('[\r\n ]{2,}', ' ', obj.__doc__)
1062 usage = getattr(obj, 'usage', '')
1063 parser.set_usage('%%prog %s [options] %s' % (command, usage))
1064 parser.epilog = getattr(obj, 'epilog', None)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001065
1066
1067def Main(argv):
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001068 """Doesn't parse the arguments here, just find the right subcommand to
1069 execute."""
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001070 try:
1071 # Do it late so all commands are listed.
1072 CMDhelp.usage = ('\n\nCommands are:\n' + '\n'.join([
1073 ' %-10s %s' % (fn[3:], Command(fn[3:]).__doc__.split('\n')[0].strip())
1074 for fn in dir(sys.modules[__name__]) if fn.startswith('CMD')]))
1075 parser = optparse.OptionParser(version='%prog ' + __version__)
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001076 parser.add_option('-v', '--verbose', action='count', default=0,
1077 help='Produces additional output for diagnostics. Can be '
1078 'used up to three times for more logging info.')
1079 parser.add_option('--gclientfile', dest='config_filename',
1080 default=os.environ.get('GCLIENT_FILE', '.gclient'),
1081 help='Specify an alternate %default file')
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001082 # Integrate standard options processing.
1083 old_parser = parser.parse_args
1084 def Parse(args):
1085 (options, args) = old_parser(args)
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001086 level = None
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001087 if options.verbose == 2:
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001088 level = logging.INFO
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001089 elif options.verbose > 2:
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001090 level = logging.DEBUG
1091 logging.basicConfig(level=level,
1092 format='%(module)s(%(lineno)d) %(funcName)s:%(message)s')
1093 options.entries_filename = options.config_filename + '_entries'
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001094 if not hasattr(options, 'revisions'):
1095 # GClient.RunOnDeps expects it even if not applicable.
1096 options.revisions = []
1097 if not hasattr(options, 'head'):
1098 options.head = None
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001099 if not hasattr(options, 'nohooks'):
1100 options.nohooks = True
1101 if not hasattr(options, 'deps_os'):
1102 options.deps_os = None
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001103 return (options, args)
1104 parser.parse_args = Parse
1105 # We don't want wordwrapping in epilog (usually examples)
1106 parser.format_epilog = lambda _: parser.epilog or ''
1107 if argv:
1108 command = Command(argv[0])
1109 if command:
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001110 # 'fix' the usage and the description now that we know the subcommand.
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001111 GenUsage(parser, argv[0])
1112 return command(parser, argv[1:])
1113 # Not a known command. Default to help.
1114 GenUsage(parser, 'help')
1115 return CMDhelp(parser, argv)
1116 except gclient_utils.Error, e:
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001117 print >> sys.stderr, 'Error: %s' % str(e)
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001118 return 1
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001119
1120
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001121if '__main__' == __name__:
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001122 sys.exit(Main(sys.argv[1:]))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001123
1124# vim: ts=2:sw=2:tw=80:et: