blob: 3ae9ffecf06430947f1cc64701b30287810521f7 [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.
maruel@chromium.org6e55ebe2010-07-07 19:35:37 +0000365 while (self.file_list[i].startswith('\\') or
366 self.file_list[i].startswith('/')):
maruel@chromium.orgadecb312010-07-07 19:31:49 +0000367 self.file_list[i] = self.file_list[i][1:]
368
369 # Run hooks on the basis of whether the files from the gclient operation
370 # match each hook's pattern.
371 for hook_dict in self.deps_hooks:
372 pattern = re.compile(hook_dict['pattern'])
373 matching_file_list = [f for f in self.file_list if pattern.search(f)]
374 if matching_file_list:
375 self._RunHookAction(hook_dict, matching_file_list)
376 if self.recursion_limit():
377 for s in self.dependencies:
378 s.RunHooksRecursively(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000379
maruel@chromium.orgeaf61062010-07-07 18:42:39 +0000380 def _RunHookAction(self, hook_dict, matching_file_list):
381 """Runs the action from a single hook."""
382 logging.info(hook_dict)
383 logging.info(matching_file_list)
384 command = hook_dict['action'][:]
385 if command[0] == 'python':
386 # If the hook specified "python" as the first item, the action is a
387 # Python script. Run it by starting a new copy of the same
388 # interpreter.
389 command[0] = sys.executable
390
391 if '$matching_files' in command:
392 splice_index = command.index('$matching_files')
393 command[splice_index:splice_index + 1] = matching_file_list
394
395 # Use a discrete exit status code of 2 to indicate that a hook action
396 # failed. Users of this script may wish to treat hook action failures
397 # differently from VC failures.
398 return gclient_utils.SubprocessCall(command, self.root_dir(), fail_status=2)
399
maruel@chromium.org271375b2010-06-23 19:17:38 +0000400 def root_dir(self):
401 return self.parent.root_dir()
402
403 def enforced_os(self):
404 return self.parent.enforced_os()
405
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000406 def recursion_limit(self):
407 return self.parent.recursion_limit() - 1
408
409 def tree(self, force_all):
410 return self.parent.tree(force_all)
411
412 def get_custom_deps(self, name, url):
413 """Returns a custom deps if applicable."""
414 if self.parent:
415 url = self.parent.get_custom_deps(name, url)
416 # None is a valid return value to disable a dependency.
417 return self.custom_deps.get(name, url)
418
419 def __str__(self):
420 out = []
421 for i in ('name', 'url', 'safesync_url', 'custom_deps', 'custom_vars',
maruel@chromium.orgadecb312010-07-07 19:31:49 +0000422 'deps_hooks', 'file_list'):
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000423 # 'deps_file'
424 if self.__dict__[i]:
425 out.append('%s: %s' % (i, self.__dict__[i]))
426
427 for d in self.dependencies:
428 out.extend([' ' + x for x in str(d).splitlines()])
429 out.append('')
430 return '\n'.join(out)
431
432 def __repr__(self):
433 return '%s: %s' % (self.name, self.url)
434
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000435
436class GClient(Dependency):
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000437 """Object that represent a gclient checkout. A tree of Dependency(), one per
438 solution or DEPS entry."""
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000439
440 DEPS_OS_CHOICES = {
441 "win32": "win",
442 "win": "win",
443 "cygwin": "win",
444 "darwin": "mac",
445 "mac": "mac",
446 "unix": "unix",
447 "linux": "unix",
448 "linux2": "unix",
449 }
450
451 DEFAULT_CLIENT_FILE_TEXT = ("""\
452solutions = [
453 { "name" : "%(solution_name)s",
454 "url" : "%(solution_url)s",
455 "custom_deps" : {
456 },
maruel@chromium.org73e21142010-07-05 13:32:01 +0000457 "safesync_url": "%(safesync_url)s",
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000458 },
459]
460""")
461
462 DEFAULT_SNAPSHOT_SOLUTION_TEXT = ("""\
463 { "name" : "%(solution_name)s",
464 "url" : "%(solution_url)s",
465 "custom_deps" : {
maruel@chromium.org73e21142010-07-05 13:32:01 +0000466%(solution_deps)s },
467 "safesync_url": "%(safesync_url)s",
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000468 },
469""")
470
471 DEFAULT_SNAPSHOT_FILE_TEXT = ("""\
472# Snapshot generated with gclient revinfo --snapshot
473solutions = [
maruel@chromium.org73e21142010-07-05 13:32:01 +0000474%(solution_list)s]
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000475""")
476
477 def __init__(self, root_dir, options):
478 Dependency.__init__(self, None, None, None)
maruel@chromium.org0d425922010-06-21 19:22:24 +0000479 self._options = options
maruel@chromium.org271375b2010-06-23 19:17:38 +0000480 if options.deps_os:
481 enforced_os = options.deps_os.split(',')
482 else:
483 enforced_os = [self.DEPS_OS_CHOICES.get(sys.platform, 'unix')]
484 if 'all' in enforced_os:
485 enforced_os = self.DEPS_OS_CHOICES.itervalues()
486 self._enforced_os = list(set(enforced_os))
487 self._root_dir = root_dir
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000488 self.config_content = None
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000489 # Do not change previous behavior. Only solution level and immediate DEPS
490 # are processed.
491 self._recursion_limit = 2
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000492
493 def SetConfig(self, content):
494 assert self.dependencies == []
495 config_dict = {}
496 self.config_content = content
497 try:
498 exec(content, config_dict)
499 except SyntaxError, e:
maruel@chromium.org5990f9d2010-07-07 18:02:58 +0000500 gclient_utils.SyntaxErrorToError('.gclient', e)
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000501 for s in config_dict.get('solutions', []):
maruel@chromium.org81843b82010-06-28 16:49:26 +0000502 try:
503 self.dependencies.append(Dependency(
504 self, s['name'], s['url'],
505 s.get('safesync_url', None),
506 s.get('custom_deps', {}),
507 s.get('custom_vars', {})))
508 except KeyError:
509 raise gclient_utils.Error('Invalid .gclient file. Solution is '
510 'incomplete: %s' % s)
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000511 # .gclient can have hooks.
512 self.deps_hooks = config_dict.get('hooks', [])
513
514 def SaveConfig(self):
515 gclient_utils.FileWrite(os.path.join(self.root_dir(),
516 self._options.config_filename),
517 self.config_content)
518
519 @staticmethod
520 def LoadCurrentConfig(options):
521 """Searches for and loads a .gclient file relative to the current working
522 dir. Returns a GClient object."""
523 path = gclient_utils.FindGclientRoot(os.getcwd(), options.config_filename)
524 if not path:
525 return None
526 client = GClient(path, options)
527 client.SetConfig(gclient_utils.FileRead(
528 os.path.join(path, options.config_filename)))
529 return client
530
531 def SetDefaultConfig(self, solution_name, solution_url, safesync_url):
532 self.SetConfig(self.DEFAULT_CLIENT_FILE_TEXT % {
533 'solution_name': solution_name,
534 'solution_url': solution_url,
535 'safesync_url' : safesync_url,
536 })
537
maruel@chromium.orgadecb312010-07-07 19:31:49 +0000538 def _SaveEntries(self):
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000539 """Creates a .gclient_entries file to record the list of unique checkouts.
540
541 The .gclient_entries file lives in the same directory as .gclient.
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000542 """
543 # Sometimes pprint.pformat will use {', sometimes it'll use { ' ... It
544 # makes testing a bit too fun.
maruel@chromium.orgadecb312010-07-07 19:31:49 +0000545 result = 'entries = {\n'
546 for entry in self.tree(False):
547 result += ' %s: %s,\n' % (pprint.pformat(entry.name),
548 pprint.pformat(entry.parsed_url))
549 result += '}\n'
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000550 file_path = os.path.join(self.root_dir(), self._options.entries_filename)
maruel@chromium.orgadecb312010-07-07 19:31:49 +0000551 logging.info(result)
552 gclient_utils.FileWrite(file_path, result)
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000553
554 def _ReadEntries(self):
555 """Read the .gclient_entries file for the given client.
556
557 Returns:
558 A sequence of solution names, which will be empty if there is the
559 entries file hasn't been created yet.
560 """
561 scope = {}
562 filename = os.path.join(self.root_dir(), self._options.entries_filename)
563 if not os.path.exists(filename):
maruel@chromium.org73e21142010-07-05 13:32:01 +0000564 return {}
maruel@chromium.org5990f9d2010-07-07 18:02:58 +0000565 try:
566 exec(gclient_utils.FileRead(filename), scope)
567 except SyntaxError, e:
568 gclient_utils.SyntaxErrorToError(filename, e)
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000569 return scope['entries']
570
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000571 def _EnforceRevisions(self):
maruel@chromium.org918a9ae2010-05-28 15:50:30 +0000572 """Checks for revision overrides."""
573 revision_overrides = {}
maruel@chromium.org307d1792010-05-31 20:03:13 +0000574 if self._options.head:
575 return revision_overrides
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000576 for s in self.dependencies:
577 if not s.safesync_url:
maruel@chromium.org307d1792010-05-31 20:03:13 +0000578 continue
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000579 handle = urllib.urlopen(s.safesync_url)
maruel@chromium.org307d1792010-05-31 20:03:13 +0000580 rev = handle.read().strip()
581 handle.close()
582 if len(rev):
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000583 self._options.revisions.append('%s@%s' % (s.name, rev))
maruel@chromium.org307d1792010-05-31 20:03:13 +0000584 if not self._options.revisions:
585 return revision_overrides
586 # --revision will take over safesync_url.
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000587 solutions_names = [s.name for s in self.dependencies]
maruel@chromium.org307d1792010-05-31 20:03:13 +0000588 index = 0
589 for revision in self._options.revisions:
590 if not '@' in revision:
591 # Support for --revision 123
592 revision = '%s@%s' % (solutions_names[index], revision)
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000593 sol, rev = revision.split('@', 1)
maruel@chromium.org307d1792010-05-31 20:03:13 +0000594 if not sol in solutions_names:
595 #raise gclient_utils.Error('%s is not a valid solution.' % sol)
596 print >> sys.stderr, ('Please fix your script, having invalid '
597 '--revision flags will soon considered an error.')
598 else:
maruel@chromium.org918a9ae2010-05-28 15:50:30 +0000599 revision_overrides[sol] = rev
maruel@chromium.org307d1792010-05-31 20:03:13 +0000600 index += 1
maruel@chromium.org918a9ae2010-05-28 15:50:30 +0000601 return revision_overrides
602
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000603 def RunOnDeps(self, command, args):
604 """Runs a command on each dependency in a client and its dependencies.
605
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000606 Args:
607 command: The command to use (e.g., 'status' or 'diff')
608 args: list of str - extra arguments to add to the command line.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000609 """
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000610 if not self.dependencies:
maruel@chromium.org73e21142010-07-05 13:32:01 +0000611 raise gclient_utils.Error('No solution specified')
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000612 revision_overrides = self._EnforceRevisions()
maruel@chromium.orgadecb312010-07-07 19:31:49 +0000613 pm = None
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000614 if command == 'update' and not self._options.verbose:
maruel@chromium.orgadecb312010-07-07 19:31:49 +0000615 pm = Progress('Syncing projects', len(self.tree(False)) + 1)
616 self.RunCommandRecursively(self._options, revision_overrides,
617 command, args, pm)
618 if pm:
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000619 pm.end()
piman@chromium.org6f363722010-04-27 00:41:09 +0000620
maruel@chromium.orgadecb312010-07-07 19:31:49 +0000621 # Once all the dependencies have been processed, it's now safe to run the
622 # hooks.
623 if not self._options.nohooks:
624 self.RunHooksRecursively(self._options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000625
626 if command == 'update':
ajwong@chromium.orgcdcee802009-06-23 15:30:42 +0000627 # Notify the user if there is an orphaned entry in their working copy.
628 # Only delete the directory if there are no changes in it, and
629 # delete_unversioned_trees is set to true.
maruel@chromium.orgadecb312010-07-07 19:31:49 +0000630 entries = [i.name for i in self.tree(False)]
631 for entry, prev_url in self._ReadEntries().iteritems():
maruel@chromium.orgc5e9aec2009-08-03 18:25:56 +0000632 # Fix path separator on Windows.
633 entry_fixed = entry.replace('/', os.path.sep)
maruel@chromium.org75a59272010-06-11 22:34:03 +0000634 e_dir = os.path.join(self.root_dir(), entry_fixed)
maruel@chromium.orgc5e9aec2009-08-03 18:25:56 +0000635 # Use entry and not entry_fixed there.
maruel@chromium.org0329e672009-05-13 18:41:04 +0000636 if entry not in entries and os.path.exists(e_dir):
maruel@chromium.orgadecb312010-07-07 19:31:49 +0000637 file_list = []
638 scm = gclient_scm.CreateSCM(prev_url, self.root_dir(), entry_fixed)
639 scm.status(self._options, [], file_list)
640 modified_files = file_list != []
msb@chromium.org83017012009-09-28 18:52:12 +0000641 if not self._options.delete_unversioned_trees or modified_files:
maruel@chromium.orgc5e9aec2009-08-03 18:25:56 +0000642 # There are modified files in this entry. Keep warning until
643 # removed.
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000644 print(('\nWARNING: \'%s\' is no longer part of this client. '
645 'It is recommended that you manually remove it.\n') %
maruel@chromium.orgc5e9aec2009-08-03 18:25:56 +0000646 entry_fixed)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000647 else:
648 # Delete the entry
maruel@chromium.org73e21142010-07-05 13:32:01 +0000649 print('\n________ deleting \'%s\' in \'%s\'' % (
650 entry_fixed, self.root_dir()))
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000651 gclient_utils.RemoveDirectory(e_dir)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000652 # record the current list of entries for next time
maruel@chromium.orgadecb312010-07-07 19:31:49 +0000653 self._SaveEntries()
maruel@chromium.org17cdf762010-05-28 17:30:52 +0000654 return 0
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000655
656 def PrintRevInfo(self):
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000657 """Output revision info mapping for the client and its dependencies.
658
659 This allows the capture of an overall "revision" for the source tree that
maruel@chromium.org918a9ae2010-05-28 15:50:30 +0000660 can be used to reproduce the same tree in the future. It is only useful for
661 "unpinned dependencies", i.e. DEPS/deps references without a svn revision
662 number or a git hash. A git branch name isn't "pinned" since the actual
663 commit can change.
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000664
665 The --snapshot option allows creating a .gclient file to reproduce the tree.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000666 """
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000667 if not self.dependencies:
maruel@chromium.org73e21142010-07-05 13:32:01 +0000668 raise gclient_utils.Error('No solution specified')
maruel@chromium.orgadecb312010-07-07 19:31:49 +0000669 # Load all the settings.
670 self.RunCommandRecursively(self._options, {}, None, [], None)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000671
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000672 def GetURLAndRev(name, original_url):
maruel@chromium.orgadecb312010-07-07 19:31:49 +0000673 """Returns the revision-qualified SCM url."""
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000674 url, _ = gclient_utils.SplitUrlRevision(original_url)
maruel@chromium.org75a59272010-06-11 22:34:03 +0000675 scm = gclient_scm.CreateSCM(original_url, self.root_dir(), name)
maruel@chromium.orgadecb312010-07-07 19:31:49 +0000676 if not os.path.isdir(scm.checkout_path):
677 return None
678 return '%s@%s' % (url, scm.revinfo(self._options, [], None))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000679
nasser@codeaurora.orgde8f3522010-03-11 23:47:44 +0000680 if self._options.snapshot:
maruel@chromium.orgadecb312010-07-07 19:31:49 +0000681 new_gclient = ''
682 # First level at .gclient
683 for d in self.dependencies:
684 entries = {}
685 def GrabDeps(sol):
686 """Recursively grab dependencies."""
687 for i in sol.dependencies:
688 entries[i.name] = GetURLAndRev(i.name, i.parsed_url)
689 GrabDeps(i)
690 GrabDeps(d)
691 custom_deps = []
692 for k in sorted(entries.keys()):
693 if entries[k]:
694 # Quotes aren't escaped...
695 custom_deps.append(' \"%s\": \'%s\',\n' % (k, entries[k]))
696 else:
697 custom_deps.append(' \"%s\": None,\n' % k)
698 new_gclient += self.DEFAULT_SNAPSHOT_SOLUTION_TEXT % {
699 'solution_name': d.name,
700 'solution_url': d.url,
701 'safesync_url' : d.safesync_url or '',
702 'solution_deps': ''.join(custom_deps),
703 }
704 # Print the snapshot configuration file
705 print(self.DEFAULT_SNAPSHOT_FILE_TEXT % {'solution_list': new_gclient})
nasser@codeaurora.orgde8f3522010-03-11 23:47:44 +0000706 else:
maruel@chromium.orgadecb312010-07-07 19:31:49 +0000707 entries = sorted(self.tree(False), key=lambda i: i.name)
708 for entry in entries:
709 revision = GetURLAndRev(entry.name, entry.parsed_url)
710 line = '%s: %s' % (entry.name, revision)
711 if not entry is entries[-1]:
712 line += ';'
713 print line
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000714
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000715 def ParseDepsFile(self, direct_reference):
716 """No DEPS to parse for a .gclient file."""
717 self.direct_reference = direct_reference
718 self.deps_parsed = True
719
maruel@chromium.org75a59272010-06-11 22:34:03 +0000720 def root_dir(self):
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000721 """Root directory of gclient checkout."""
maruel@chromium.org75a59272010-06-11 22:34:03 +0000722 return self._root_dir
723
maruel@chromium.org271375b2010-06-23 19:17:38 +0000724 def enforced_os(self):
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000725 """What deps_os entries that are to be parsed."""
maruel@chromium.org271375b2010-06-23 19:17:38 +0000726 return self._enforced_os
727
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000728 def recursion_limit(self):
729 """How recursive can each dependencies in DEPS file can load DEPS file."""
730 return self._recursion_limit
731
732 def tree(self, force_all):
733 """Returns a flat list of all the dependencies."""
734 def subtree(dep):
735 if not force_all and not dep.direct_reference:
736 # Was loaded from a From() keyword in a DEPS file, don't load all its
737 # dependencies.
738 return []
739 result = dep.dependencies[:]
740 for d in dep.dependencies:
741 result.extend(subtree(d))
742 return result
743 return subtree(self)
744
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000745
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000746#### gclient commands.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000747
748
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000749def CMDcleanup(parser, args):
750 """Cleans up all working copies.
maruel@chromium.orgddff62d2010-05-17 21:02:36 +0000751
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000752Mostly svn-specific. Simply runs 'svn cleanup' for each module.
maruel@chromium.org79692d62010-05-14 18:57:13 +0000753"""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000754 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
755 help='override deps for the specified (comma-separated) '
756 'platform(s); \'all\' will process all deps_os '
757 'references')
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000758 (options, args) = parser.parse_args(args)
maruel@chromium.org2806acc2009-05-15 12:33:34 +0000759 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000760 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000761 raise gclient_utils.Error('client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000762 if options.verbose:
763 # Print out the .gclient file. This is longer than if we just printed the
764 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.org116704f2010-06-11 17:34:38 +0000765 print(client.config_content)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000766 return client.RunOnDeps('cleanup', args)
767
768
piman@chromium.org4b90e3a2010-07-01 20:28:26 +0000769@attr('usage', '[command] [args ...]')
770def CMDrecurse(parser, args):
771 """Operates on all the entries.
772
773 Runs a shell command on all entries.
774 """
775 # Stop parsing at the first non-arg so that these go through to the command
776 parser.disable_interspersed_args()
777 parser.add_option('-s', '--scm', action='append', default=[],
778 help='choose scm types to operate upon')
779 options, args = parser.parse_args(args)
780 root, entries = gclient_utils.GetGClientRootAndEntries()
781 scm_set = set()
782 for scm in options.scm:
783 scm_set.update(scm.split(','))
784
785 # Pass in the SCM type as an env variable
786 env = os.environ.copy()
787
788 for path, url in entries.iteritems():
789 scm = gclient_scm.GetScmName(url)
790 if scm_set and scm not in scm_set:
791 continue
792 dir = os.path.normpath(os.path.join(root, path))
793 env['GCLIENT_SCM'] = scm
794 env['GCLIENT_URL'] = url
795 subprocess.Popen(args, cwd=dir, env=env).communicate()
796
797
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000798@attr('usage', '[url] [safesync url]')
799def CMDconfig(parser, args):
maruel@chromium.orgddff62d2010-05-17 21:02:36 +0000800 """Create a .gclient file in the current directory.
801
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000802This specifies the configuration for further commands. After update/sync,
maruel@chromium.org79692d62010-05-14 18:57:13 +0000803top-level DEPS files in each module are read to determine dependent
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000804modules to operate on as well. If optional [url] parameter is
maruel@chromium.org79692d62010-05-14 18:57:13 +0000805provided, then configuration is read from a specified Subversion server
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000806URL.
maruel@chromium.org79692d62010-05-14 18:57:13 +0000807"""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000808 parser.add_option('--spec',
809 help='create a gclient file containing the provided '
810 'string. Due to Cygwin/Python brokenness, it '
811 'probably can\'t contain any newlines.')
812 parser.add_option('--name',
813 help='overrides the default name for the solution')
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000814 (options, args) = parser.parse_args(args)
maruel@chromium.org5fc2a332010-05-26 19:37:15 +0000815 if ((options.spec and args) or len(args) > 2 or
816 (not options.spec and not args)):
817 parser.error('Inconsistent arguments. Use either --spec or one or 2 args')
818
maruel@chromium.org0329e672009-05-13 18:41:04 +0000819 if os.path.exists(options.config_filename):
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000820 raise gclient_utils.Error('%s file already exists in the current directory'
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000821 % options.config_filename)
maruel@chromium.org2806acc2009-05-15 12:33:34 +0000822 client = GClient('.', options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000823 if options.spec:
824 client.SetConfig(options.spec)
825 else:
maruel@chromium.org1ab7ffc2009-06-03 17:21:37 +0000826 base_url = args[0].rstrip('/')
iposva@chromium.org8cf7a392010-04-07 17:20:26 +0000827 if not options.name:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000828 name = base_url.split('/')[-1]
iposva@chromium.org8cf7a392010-04-07 17:20:26 +0000829 else:
830 # specify an alternate relpath for the given URL.
831 name = options.name
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000832 safesync_url = ''
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000833 if len(args) > 1:
834 safesync_url = args[1]
835 client.SetDefaultConfig(name, base_url, safesync_url)
836 client.SaveConfig()
maruel@chromium.org79692d62010-05-14 18:57:13 +0000837 return 0
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000838
839
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000840def CMDexport(parser, args):
maruel@chromium.orgddff62d2010-05-17 21:02:36 +0000841 """Wrapper for svn export for all managed directories."""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000842 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
843 help='override deps for the specified (comma-separated) '
844 'platform(s); \'all\' will process all deps_os '
845 'references')
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000846 (options, args) = parser.parse_args(args)
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +0000847 if len(args) != 1:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000848 raise gclient_utils.Error('Need directory name')
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +0000849 client = GClient.LoadCurrentConfig(options)
850
851 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000852 raise gclient_utils.Error('client not configured; see \'gclient config\'')
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +0000853
854 if options.verbose:
855 # Print out the .gclient file. This is longer than if we just printed the
856 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.org116704f2010-06-11 17:34:38 +0000857 print(client.config_content)
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +0000858 return client.RunOnDeps('export', args)
859
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000860
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000861@attr('epilog', """Example:
862 gclient pack > patch.txt
863 generate simple patch for configured client and dependences
864""")
865def CMDpack(parser, args):
maruel@chromium.org79692d62010-05-14 18:57:13 +0000866 """Generate a patch which can be applied at the root of the tree.
maruel@chromium.orgddff62d2010-05-17 21:02:36 +0000867
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000868Internally, runs 'svn diff'/'git diff' on each checked out module and
maruel@chromium.org79692d62010-05-14 18:57:13 +0000869dependencies, and performs minimal postprocessing of the output. The
870resulting patch is printed to stdout and can be applied to a freshly
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000871checked out tree via 'patch -p0 < patchfile'.
maruel@chromium.org79692d62010-05-14 18:57:13 +0000872"""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000873 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
874 help='override deps for the specified (comma-separated) '
875 'platform(s); \'all\' will process all deps_os '
876 'references')
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000877 (options, args) = parser.parse_args(args)
kbr@google.comab318592009-09-04 00:54:55 +0000878 client = GClient.LoadCurrentConfig(options)
879 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000880 raise gclient_utils.Error('client not configured; see \'gclient config\'')
kbr@google.comab318592009-09-04 00:54:55 +0000881 if options.verbose:
882 # Print out the .gclient file. This is longer than if we just printed the
883 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.org116704f2010-06-11 17:34:38 +0000884 print(client.config_content)
kbr@google.comab318592009-09-04 00:54:55 +0000885 return client.RunOnDeps('pack', args)
886
887
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000888def CMDstatus(parser, args):
889 """Show modification status for every dependencies."""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000890 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
891 help='override deps for the specified (comma-separated) '
892 'platform(s); \'all\' will process all deps_os '
893 'references')
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000894 (options, args) = parser.parse_args(args)
maruel@chromium.org2806acc2009-05-15 12:33:34 +0000895 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000896 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000897 raise gclient_utils.Error('client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000898 if options.verbose:
899 # Print out the .gclient file. This is longer than if we just printed the
900 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.org116704f2010-06-11 17:34:38 +0000901 print(client.config_content)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000902 return client.RunOnDeps('status', args)
903
904
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000905@attr('epilog', """Examples:
maruel@chromium.org79692d62010-05-14 18:57:13 +0000906 gclient sync
907 update files from SCM according to current configuration,
908 *for modules which have changed since last update or sync*
909 gclient sync --force
910 update files from SCM according to current configuration, for
911 all modules (useful for recovering files deleted from local copy)
912 gclient sync --revision src@31000
913 update src directory to r31000
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000914""")
915def CMDsync(parser, args):
916 """Checkout/update all modules."""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000917 parser.add_option('-f', '--force', action='store_true',
918 help='force update even for unchanged modules')
919 parser.add_option('-n', '--nohooks', action='store_true',
920 help='don\'t run hooks after the update is complete')
921 parser.add_option('-r', '--revision', action='append',
922 dest='revisions', metavar='REV', default=[],
923 help='Enforces revision/hash for the solutions with the '
924 'format src@rev. The src@ part is optional and can be '
925 'skipped. -r can be used multiple times when .gclient '
926 'has multiple solutions configured and will work even '
927 'if the src@ part is skipped.')
928 parser.add_option('-H', '--head', action='store_true',
929 help='skips any safesync_urls specified in '
930 'configured solutions and sync to head instead')
931 parser.add_option('-D', '--delete_unversioned_trees', action='store_true',
932 help='delete any unexpected unversioned trees '
933 'that are in the checkout')
934 parser.add_option('-R', '--reset', action='store_true',
935 help='resets any local changes before updating (git only)')
936 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
937 help='override deps for the specified (comma-separated) '
938 'platform(s); \'all\' will process all deps_os '
939 'references')
940 parser.add_option('-m', '--manually_grab_svn_rev', action='store_true',
941 help='Skip svn up whenever possible by requesting '
942 'actual HEAD revision from the repository')
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000943 (options, args) = parser.parse_args(args)
maruel@chromium.org2806acc2009-05-15 12:33:34 +0000944 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000945
946 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000947 raise gclient_utils.Error('client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000948
maruel@chromium.org307d1792010-05-31 20:03:13 +0000949 if options.revisions and options.head:
950 # TODO(maruel): Make it a parser.error if it doesn't break any builder.
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000951 print('Warning: you cannot use both --head and --revision')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000952
953 if options.verbose:
954 # Print out the .gclient file. This is longer than if we just printed the
955 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.org116704f2010-06-11 17:34:38 +0000956 print(client.config_content)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000957 return client.RunOnDeps('update', args)
958
959
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000960def CMDupdate(parser, args):
maruel@chromium.orgddff62d2010-05-17 21:02:36 +0000961 """Alias for the sync command. Deprecated."""
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000962 return CMDsync(parser, args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000963
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000964def CMDdiff(parser, args):
965 """Displays local diff for every dependencies."""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000966 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
967 help='override deps for the specified (comma-separated) '
968 'platform(s); \'all\' will process all deps_os '
969 'references')
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000970 (options, args) = parser.parse_args(args)
maruel@chromium.org2806acc2009-05-15 12:33:34 +0000971 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000972 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000973 raise gclient_utils.Error('client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000974 if options.verbose:
975 # Print out the .gclient file. This is longer than if we just printed the
976 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.org116704f2010-06-11 17:34:38 +0000977 print(client.config_content)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000978 return client.RunOnDeps('diff', args)
979
980
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000981def CMDrevert(parser, args):
982 """Revert all modifications in every dependencies."""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000983 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
984 help='override deps for the specified (comma-separated) '
985 'platform(s); \'all\' will process all deps_os '
986 'references')
987 parser.add_option('-n', '--nohooks', action='store_true',
988 help='don\'t run hooks after the revert is complete')
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000989 (options, args) = parser.parse_args(args)
990 # --force is implied.
991 options.force = True
maruel@chromium.org2806acc2009-05-15 12:33:34 +0000992 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000993 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000994 raise gclient_utils.Error('client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000995 return client.RunOnDeps('revert', args)
996
997
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000998def CMDrunhooks(parser, args):
999 """Runs hooks for files that have been modified in the local working copy."""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00001000 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
1001 help='override deps for the specified (comma-separated) '
1002 'platform(s); \'all\' will process all deps_os '
1003 'references')
1004 parser.add_option('-f', '--force', action='store_true', default=True,
1005 help='Deprecated. No effect.')
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001006 (options, args) = parser.parse_args(args)
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001007 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001008 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00001009 raise gclient_utils.Error('client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001010 if options.verbose:
1011 # Print out the .gclient file. This is longer than if we just printed the
1012 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.org116704f2010-06-11 17:34:38 +00001013 print(client.config_content)
maruel@chromium.org5df6a462009-08-28 18:52:26 +00001014 options.force = True
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001015 options.nohooks = False
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001016 return client.RunOnDeps('runhooks', args)
1017
1018
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001019def CMDrevinfo(parser, args):
maruel@chromium.org9eda4112010-06-11 18:56:10 +00001020 """Output revision info mapping for the client and its dependencies.
1021
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00001022 This allows the capture of an overall 'revision' for the source tree that
maruel@chromium.org9eda4112010-06-11 18:56:10 +00001023 can be used to reproduce the same tree in the future. It is only useful for
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00001024 'unpinned dependencies', i.e. DEPS/deps references without a svn revision
1025 number or a git hash. A git branch name isn't 'pinned' since the actual
maruel@chromium.org9eda4112010-06-11 18:56:10 +00001026 commit can change.
1027 """
1028 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
1029 help='override deps for the specified (comma-separated) '
1030 'platform(s); \'all\' will process all deps_os '
1031 'references')
1032 parser.add_option('-s', '--snapshot', action='store_true',
1033 help='creates a snapshot .gclient file of the current '
maruel@chromium.orgadecb312010-07-07 19:31:49 +00001034 'version of all repositories to reproduce the tree')
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001035 (options, args) = parser.parse_args(args)
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001036 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001037 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00001038 raise gclient_utils.Error('client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001039 client.PrintRevInfo()
maruel@chromium.org79692d62010-05-14 18:57:13 +00001040 return 0
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001041
1042
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001043def Command(name):
1044 return getattr(sys.modules[__name__], 'CMD' + name, None)
1045
1046
1047def CMDhelp(parser, args):
1048 """Prints list of commands or help for a specific command."""
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001049 (_, args) = parser.parse_args(args)
maruel@chromium.orgddff62d2010-05-17 21:02:36 +00001050 if len(args) == 1:
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001051 return Main(args + ['--help'])
maruel@chromium.orgddff62d2010-05-17 21:02:36 +00001052 parser.print_help()
1053 return 0
1054
1055
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001056def GenUsage(parser, command):
1057 """Modify an OptParse object with the function's documentation."""
1058 obj = Command(command)
1059 if command == 'help':
1060 command = '<command>'
1061 # OptParser.description prefer nicely non-formatted strings.
1062 parser.description = re.sub('[\r\n ]{2,}', ' ', obj.__doc__)
1063 usage = getattr(obj, 'usage', '')
1064 parser.set_usage('%%prog %s [options] %s' % (command, usage))
1065 parser.epilog = getattr(obj, 'epilog', None)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001066
1067
1068def Main(argv):
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001069 """Doesn't parse the arguments here, just find the right subcommand to
1070 execute."""
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001071 try:
1072 # Do it late so all commands are listed.
1073 CMDhelp.usage = ('\n\nCommands are:\n' + '\n'.join([
1074 ' %-10s %s' % (fn[3:], Command(fn[3:]).__doc__.split('\n')[0].strip())
1075 for fn in dir(sys.modules[__name__]) if fn.startswith('CMD')]))
1076 parser = optparse.OptionParser(version='%prog ' + __version__)
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001077 parser.add_option('-v', '--verbose', action='count', default=0,
1078 help='Produces additional output for diagnostics. Can be '
1079 'used up to three times for more logging info.')
1080 parser.add_option('--gclientfile', dest='config_filename',
1081 default=os.environ.get('GCLIENT_FILE', '.gclient'),
1082 help='Specify an alternate %default file')
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001083 # Integrate standard options processing.
1084 old_parser = parser.parse_args
1085 def Parse(args):
1086 (options, args) = old_parser(args)
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001087 level = None
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001088 if options.verbose == 2:
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001089 level = logging.INFO
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001090 elif options.verbose > 2:
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001091 level = logging.DEBUG
1092 logging.basicConfig(level=level,
1093 format='%(module)s(%(lineno)d) %(funcName)s:%(message)s')
1094 options.entries_filename = options.config_filename + '_entries'
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001095 if not hasattr(options, 'revisions'):
1096 # GClient.RunOnDeps expects it even if not applicable.
1097 options.revisions = []
1098 if not hasattr(options, 'head'):
1099 options.head = None
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001100 if not hasattr(options, 'nohooks'):
1101 options.nohooks = True
1102 if not hasattr(options, 'deps_os'):
1103 options.deps_os = None
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001104 return (options, args)
1105 parser.parse_args = Parse
1106 # We don't want wordwrapping in epilog (usually examples)
1107 parser.format_epilog = lambda _: parser.epilog or ''
1108 if argv:
1109 command = Command(argv[0])
1110 if command:
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001111 # 'fix' the usage and the description now that we know the subcommand.
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001112 GenUsage(parser, argv[0])
1113 return command(parser, argv[1:])
1114 # Not a known command. Default to help.
1115 GenUsage(parser, 'help')
1116 return CMDhelp(parser, argv)
1117 except gclient_utils.Error, e:
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001118 print >> sys.stderr, 'Error: %s' % str(e)
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001119 return 1
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001120
1121
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001122if '__main__' == __name__:
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001123 sys.exit(Main(sys.argv[1:]))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001124
1125# vim: ts=2:sw=2:tw=80:et: