blob: 9a722f7253e3cee2844d2574631ad60ec65f7720 [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.orgdf2b3152010-07-21 17:35:24 +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
maruel@chromium.orge3216c62010-07-08 03:31:43 +0000101 from a SVN repo."""
maruel@chromium.org116704f2010-06-11 17:34:38 +0000102
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.orgdf2b3152010-07-21 17:35:24 +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.orgdf2b3152010-07-21 17:35:24 +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.orgdf2b3152010-07-21 17:35:24 +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.orgdf2b3152010-07-21 17:35:24 +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 parent_url = self.parent.parsed_url
212 if isinstance(parent_url, self.FileImpl):
213 parent_url = parent_url.file_location
214 scm = gclient_scm.CreateSCM(parent_url, self.root_dir(), None)
215 self.parsed_url = scm.FullUrlForRelativeUrl(url)
216 else:
217 self.parsed_url = url
218 logging.debug('%s, %s -> %s' % (self.name, url, self.parsed_url))
219 elif isinstance(url, self.FileImpl):
220 self.parsed_url = url
221 logging.debug('%s, %s -> %s (File)' % (self.name, url, self.parsed_url))
222 return self.parsed_url
223
maruel@chromium.org271375b2010-06-23 19:17:38 +0000224 def ParseDepsFile(self, direct_reference):
225 """Parses the DEPS file for this dependency."""
226 if direct_reference:
227 # Maybe it was referenced earlier by a From() keyword but it's now
228 # directly referenced.
229 self.direct_reference = direct_reference
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000230 if self.deps_parsed:
231 return
maruel@chromium.org271375b2010-06-23 19:17:38 +0000232 self.deps_parsed = True
233 filepath = os.path.join(self.root_dir(), self.name, self.deps_file)
234 if not os.path.isfile(filepath):
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000235 return
maruel@chromium.org271375b2010-06-23 19:17:38 +0000236 deps_content = gclient_utils.FileRead(filepath)
maruel@chromium.org0d425922010-06-21 19:22:24 +0000237
maruel@chromium.org271375b2010-06-23 19:17:38 +0000238 # Eval the content.
239 # One thing is unintuitive, vars= {} must happen before Var() use.
240 local_scope = {}
241 var = self.VarImpl(self.custom_vars, local_scope)
242 global_scope = {
243 'File': self.FileImpl,
244 'From': self.FromImpl,
245 'Var': var.Lookup,
246 'deps_os': {},
247 }
maruel@chromium.org5990f9d2010-07-07 18:02:58 +0000248 try:
249 exec(deps_content, global_scope, local_scope)
250 except SyntaxError, e:
251 gclient_utils.SyntaxErrorToError(filepath, e)
maruel@chromium.org271375b2010-06-23 19:17:38 +0000252 deps = local_scope.get('deps', {})
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000253 # load os specific dependencies if defined. these dependencies may
254 # override or extend the values defined by the 'deps' member.
maruel@chromium.org271375b2010-06-23 19:17:38 +0000255 if 'deps_os' in local_scope:
256 for deps_os_key in self.enforced_os():
257 os_deps = local_scope['deps_os'].get(deps_os_key, {})
258 if len(self.enforced_os()) > 1:
259 # Ignore any conflict when including deps for more than one
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000260 # platform, so we collect the broadest set of dependencies available.
261 # We may end up with the wrong revision of something for our
262 # platform, but this is the best we can do.
263 deps.update([x for x in os_deps.items() if not x[0] in deps])
264 else:
265 deps.update(os_deps)
266
maruel@chromium.org271375b2010-06-23 19:17:38 +0000267 self.deps_hooks.extend(local_scope.get('hooks', []))
268
269 # If a line is in custom_deps, but not in the solution, we want to append
270 # this line to the solution.
271 for d in self.custom_deps:
272 if d not in deps:
273 deps[d] = self.custom_deps[d]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000274
275 # If use_relative_paths is set in the DEPS file, regenerate
276 # the dictionary using paths relative to the directory containing
277 # the DEPS file.
maruel@chromium.org271375b2010-06-23 19:17:38 +0000278 use_relative_paths = local_scope.get('use_relative_paths', False)
279 if use_relative_paths:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000280 rel_deps = {}
281 for d, url in deps.items():
282 # normpath is required to allow DEPS to use .. in their
283 # dependency local path.
maruel@chromium.org271375b2010-06-23 19:17:38 +0000284 rel_deps[os.path.normpath(os.path.join(self.name, d))] = url
285 deps = rel_deps
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000286
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000287 # Convert the deps into real Dependency.
288 for name, url in deps.iteritems():
289 if name in [s.name for s in self.dependencies]:
290 raise
291 self.dependencies.append(Dependency(self, name, url))
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000292 logging.info('Loaded: %s' % str(self))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000293
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +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 if not command in (None, 'cleanup', 'diff', 'pack', 'status'):
308 options.revision = self.parsed_url.GetRevision()
309 scm = gclient_scm.SVNWrapper(self.parsed_url.GetPath(),
310 self.root_dir(),
311 self.name)
312 scm.RunCommand('updatesingle', options,
313 args + [self.parsed_url.GetFilename()],
314 self.file_list)
315 else:
316 options.revision = revision_overrides.get(self.name)
317 scm = gclient_scm.CreateSCM(self.parsed_url, self.root_dir(), self.name)
318 scm.RunCommand(command, options, args, self.file_list)
319 self.file_list = [os.path.join(self.name, f.strip())
320 for f in self.file_list]
321 options.revision = None
322 if pm:
323 # The + 1 comes from the fact that .gclient is considered a step in
324 # itself, .i.e. this code is called one time for the .gclient. This is not
325 # conceptually correct but it simplifies code.
326 pm._total = len(self.tree(False)) + 1
327 pm.update()
328 if self.recursion_limit():
329 # Then we can parse the DEPS file.
330 self.ParseDepsFile(True)
331 if pm:
332 pm._total = len(self.tree(False)) + 1
333 pm.update(0)
334 # Parse the dependencies of this dependency.
335 for s in self.dependencies:
336 # TODO(maruel): All these can run concurrently! No need for threads,
337 # just buffer stdout&stderr on pipes and flush as they complete.
338 # Watch out for stdin.
339 s.RunCommandRecursively(options, revision_overrides, command, args, pm)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000340
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000341 def RunHooksRecursively(self, options):
342 """Evaluates all hooks, running actions as needed. RunCommandRecursively()
343 must have been called before to load the DEPS."""
maruel@chromium.orgdc7445d2010-07-09 21:05:29 +0000344 # If "--force" was specified, run all hooks regardless of what files have
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000345 # changed.
346 if self.deps_hooks and self.direct_reference:
347 # TODO(maruel): If the user is using git or git-svn, then we don't know
348 # what files have changed so we always run all hooks. It'd be nice to fix
349 # that.
350 if (options.force or
351 isinstance(self.parsed_url, self.FileImpl) or
352 gclient_scm.GetScmName(self.parsed_url) in ('git', None) or
353 os.path.isdir(os.path.join(self.root_dir(), self.name, '.git'))):
354 for hook_dict in self.deps_hooks:
355 self._RunHookAction(hook_dict, [])
356 else:
357 # TODO(phajdan.jr): We should know exactly when the paths are absolute.
358 # Convert all absolute paths to relative.
359 for i in range(len(self.file_list)):
360 # It depends on the command being executed (like runhooks vs sync).
361 if not os.path.isabs(self.file_list[i]):
362 continue
maruel@chromium.orgdc7445d2010-07-09 21:05:29 +0000363
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000364 prefix = os.path.commonprefix([self.root_dir().lower(),
365 self.file_list[i].lower()])
366 self.file_list[i] = self.file_list[i][len(prefix):]
367
368 # Strip any leading path separators.
369 while (self.file_list[i].startswith('\\') or
370 self.file_list[i].startswith('/')):
371 self.file_list[i] = self.file_list[i][1:]
372
373 # Run hooks on the basis of whether the files from the gclient operation
374 # match each hook's pattern.
375 for hook_dict in self.deps_hooks:
376 pattern = re.compile(hook_dict['pattern'])
377 matching_file_list = [f for f in self.file_list if pattern.search(f)]
378 if matching_file_list:
379 self._RunHookAction(hook_dict, matching_file_list)
380 if self.recursion_limit():
381 for s in self.dependencies:
382 s.RunHooksRecursively(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000383
maruel@chromium.orgeaf61062010-07-07 18:42:39 +0000384 def _RunHookAction(self, hook_dict, matching_file_list):
385 """Runs the action from a single hook."""
386 logging.info(hook_dict)
387 logging.info(matching_file_list)
388 command = hook_dict['action'][:]
389 if command[0] == 'python':
390 # If the hook specified "python" as the first item, the action is a
391 # Python script. Run it by starting a new copy of the same
392 # interpreter.
393 command[0] = sys.executable
394
395 if '$matching_files' in command:
396 splice_index = command.index('$matching_files')
397 command[splice_index:splice_index + 1] = matching_file_list
398
399 # Use a discrete exit status code of 2 to indicate that a hook action
400 # failed. Users of this script may wish to treat hook action failures
401 # differently from VC failures.
402 return gclient_utils.SubprocessCall(command, self.root_dir(), fail_status=2)
403
maruel@chromium.org271375b2010-06-23 19:17:38 +0000404 def root_dir(self):
405 return self.parent.root_dir()
406
407 def enforced_os(self):
408 return self.parent.enforced_os()
409
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000410 def recursion_limit(self):
411 return self.parent.recursion_limit() - 1
412
413 def tree(self, force_all):
414 return self.parent.tree(force_all)
415
416 def get_custom_deps(self, name, url):
417 """Returns a custom deps if applicable."""
418 if self.parent:
419 url = self.parent.get_custom_deps(name, url)
420 # None is a valid return value to disable a dependency.
421 return self.custom_deps.get(name, url)
422
423 def __str__(self):
424 out = []
425 for i in ('name', 'url', 'safesync_url', 'custom_deps', 'custom_vars',
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000426 'deps_hooks', 'file_list'):
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000427 # 'deps_file'
428 if self.__dict__[i]:
429 out.append('%s: %s' % (i, self.__dict__[i]))
430
431 for d in self.dependencies:
432 out.extend([' ' + x for x in str(d).splitlines()])
433 out.append('')
434 return '\n'.join(out)
435
436 def __repr__(self):
437 return '%s: %s' % (self.name, self.url)
438
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000439
440class GClient(Dependency):
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000441 """Object that represent a gclient checkout. A tree of Dependency(), one per
442 solution or DEPS entry."""
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000443
444 DEPS_OS_CHOICES = {
445 "win32": "win",
446 "win": "win",
447 "cygwin": "win",
448 "darwin": "mac",
449 "mac": "mac",
450 "unix": "unix",
451 "linux": "unix",
452 "linux2": "unix",
453 }
454
455 DEFAULT_CLIENT_FILE_TEXT = ("""\
456solutions = [
457 { "name" : "%(solution_name)s",
458 "url" : "%(solution_url)s",
459 "custom_deps" : {
460 },
maruel@chromium.org73e21142010-07-05 13:32:01 +0000461 "safesync_url": "%(safesync_url)s",
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000462 },
463]
464""")
465
466 DEFAULT_SNAPSHOT_SOLUTION_TEXT = ("""\
467 { "name" : "%(solution_name)s",
468 "url" : "%(solution_url)s",
469 "custom_deps" : {
maruel@chromium.org73e21142010-07-05 13:32:01 +0000470%(solution_deps)s },
471 "safesync_url": "%(safesync_url)s",
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000472 },
473""")
474
475 DEFAULT_SNAPSHOT_FILE_TEXT = ("""\
476# Snapshot generated with gclient revinfo --snapshot
477solutions = [
maruel@chromium.org73e21142010-07-05 13:32:01 +0000478%(solution_list)s]
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000479""")
480
481 def __init__(self, root_dir, options):
482 Dependency.__init__(self, None, None, None)
maruel@chromium.org0d425922010-06-21 19:22:24 +0000483 self._options = options
maruel@chromium.org271375b2010-06-23 19:17:38 +0000484 if options.deps_os:
485 enforced_os = options.deps_os.split(',')
486 else:
487 enforced_os = [self.DEPS_OS_CHOICES.get(sys.platform, 'unix')]
488 if 'all' in enforced_os:
489 enforced_os = self.DEPS_OS_CHOICES.itervalues()
490 self._enforced_os = list(set(enforced_os))
491 self._root_dir = root_dir
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000492 self.config_content = None
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000493 # Do not change previous behavior. Only solution level and immediate DEPS
494 # are processed.
495 self._recursion_limit = 2
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000496
497 def SetConfig(self, content):
498 assert self.dependencies == []
499 config_dict = {}
500 self.config_content = content
501 try:
502 exec(content, config_dict)
503 except SyntaxError, e:
maruel@chromium.org5990f9d2010-07-07 18:02:58 +0000504 gclient_utils.SyntaxErrorToError('.gclient', e)
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000505 for s in config_dict.get('solutions', []):
maruel@chromium.org81843b82010-06-28 16:49:26 +0000506 try:
507 self.dependencies.append(Dependency(
508 self, s['name'], s['url'],
509 s.get('safesync_url', None),
510 s.get('custom_deps', {}),
511 s.get('custom_vars', {})))
512 except KeyError:
513 raise gclient_utils.Error('Invalid .gclient file. Solution is '
514 'incomplete: %s' % s)
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000515 # .gclient can have hooks.
516 self.deps_hooks = config_dict.get('hooks', [])
517
518 def SaveConfig(self):
519 gclient_utils.FileWrite(os.path.join(self.root_dir(),
520 self._options.config_filename),
521 self.config_content)
522
523 @staticmethod
524 def LoadCurrentConfig(options):
525 """Searches for and loads a .gclient file relative to the current working
526 dir. Returns a GClient object."""
527 path = gclient_utils.FindGclientRoot(os.getcwd(), options.config_filename)
528 if not path:
529 return None
530 client = GClient(path, options)
531 client.SetConfig(gclient_utils.FileRead(
532 os.path.join(path, options.config_filename)))
533 return client
534
535 def SetDefaultConfig(self, solution_name, solution_url, safesync_url):
536 self.SetConfig(self.DEFAULT_CLIENT_FILE_TEXT % {
537 'solution_name': solution_name,
538 'solution_url': solution_url,
539 'safesync_url' : safesync_url,
540 })
541
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000542 def _SaveEntries(self):
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000543 """Creates a .gclient_entries file to record the list of unique checkouts.
544
545 The .gclient_entries file lives in the same directory as .gclient.
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000546 """
547 # Sometimes pprint.pformat will use {', sometimes it'll use { ' ... It
548 # makes testing a bit too fun.
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000549 result = 'entries = {\n'
550 for entry in self.tree(False):
551 # Skip over File() dependencies as we can't version them.
552 if not isinstance(entry.parsed_url, self.FileImpl):
553 result += ' %s: %s,\n' % (pprint.pformat(entry.name),
554 pprint.pformat(entry.parsed_url))
555 result += '}\n'
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000556 file_path = os.path.join(self.root_dir(), self._options.entries_filename)
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000557 logging.info(result)
558 gclient_utils.FileWrite(file_path, result)
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000559
560 def _ReadEntries(self):
561 """Read the .gclient_entries file for the given client.
562
563 Returns:
564 A sequence of solution names, which will be empty if there is the
565 entries file hasn't been created yet.
566 """
567 scope = {}
568 filename = os.path.join(self.root_dir(), self._options.entries_filename)
569 if not os.path.exists(filename):
maruel@chromium.org73e21142010-07-05 13:32:01 +0000570 return {}
maruel@chromium.org5990f9d2010-07-07 18:02:58 +0000571 try:
572 exec(gclient_utils.FileRead(filename), scope)
573 except SyntaxError, e:
574 gclient_utils.SyntaxErrorToError(filename, e)
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000575 return scope['entries']
576
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000577 def _EnforceRevisions(self):
maruel@chromium.org918a9ae2010-05-28 15:50:30 +0000578 """Checks for revision overrides."""
579 revision_overrides = {}
maruel@chromium.org307d1792010-05-31 20:03:13 +0000580 if self._options.head:
581 return revision_overrides
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000582 for s in self.dependencies:
583 if not s.safesync_url:
maruel@chromium.org307d1792010-05-31 20:03:13 +0000584 continue
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000585 handle = urllib.urlopen(s.safesync_url)
maruel@chromium.org307d1792010-05-31 20:03:13 +0000586 rev = handle.read().strip()
587 handle.close()
588 if len(rev):
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000589 self._options.revisions.append('%s@%s' % (s.name, rev))
maruel@chromium.org307d1792010-05-31 20:03:13 +0000590 if not self._options.revisions:
591 return revision_overrides
592 # --revision will take over safesync_url.
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000593 solutions_names = [s.name for s in self.dependencies]
maruel@chromium.org307d1792010-05-31 20:03:13 +0000594 index = 0
595 for revision in self._options.revisions:
596 if not '@' in revision:
597 # Support for --revision 123
598 revision = '%s@%s' % (solutions_names[index], revision)
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000599 sol, rev = revision.split('@', 1)
maruel@chromium.org307d1792010-05-31 20:03:13 +0000600 if not sol in solutions_names:
601 #raise gclient_utils.Error('%s is not a valid solution.' % sol)
602 print >> sys.stderr, ('Please fix your script, having invalid '
603 '--revision flags will soon considered an error.')
604 else:
maruel@chromium.org918a9ae2010-05-28 15:50:30 +0000605 revision_overrides[sol] = rev
maruel@chromium.org307d1792010-05-31 20:03:13 +0000606 index += 1
maruel@chromium.org918a9ae2010-05-28 15:50:30 +0000607 return revision_overrides
608
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000609 def RunOnDeps(self, command, args):
610 """Runs a command on each dependency in a client and its dependencies.
611
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000612 Args:
613 command: The command to use (e.g., 'status' or 'diff')
614 args: list of str - extra arguments to add to the command line.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000615 """
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000616 if not self.dependencies:
maruel@chromium.org73e21142010-07-05 13:32:01 +0000617 raise gclient_utils.Error('No solution specified')
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000618 revision_overrides = self._EnforceRevisions()
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000619 pm = None
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000620 if command == 'update' and not self._options.verbose:
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000621 pm = Progress('Syncing projects', len(self.tree(False)) + 1)
622 self.RunCommandRecursively(self._options, revision_overrides,
623 command, args, pm)
624 if pm:
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000625 pm.end()
piman@chromium.org6f363722010-04-27 00:41:09 +0000626
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000627 # Once all the dependencies have been processed, it's now safe to run the
628 # hooks.
629 if not self._options.nohooks:
630 self.RunHooksRecursively(self._options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000631
632 if command == 'update':
ajwong@chromium.orgcdcee802009-06-23 15:30:42 +0000633 # Notify the user if there is an orphaned entry in their working copy.
634 # Only delete the directory if there are no changes in it, and
635 # delete_unversioned_trees is set to true.
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000636 entries = [i.name for i in self.tree(False)]
637 for entry, prev_url in self._ReadEntries().iteritems():
maruel@chromium.orgc5e9aec2009-08-03 18:25:56 +0000638 # Fix path separator on Windows.
639 entry_fixed = entry.replace('/', os.path.sep)
maruel@chromium.org75a59272010-06-11 22:34:03 +0000640 e_dir = os.path.join(self.root_dir(), entry_fixed)
maruel@chromium.orgc5e9aec2009-08-03 18:25:56 +0000641 # Use entry and not entry_fixed there.
maruel@chromium.org0329e672009-05-13 18:41:04 +0000642 if entry not in entries and os.path.exists(e_dir):
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000643 file_list = []
644 scm = gclient_scm.CreateSCM(prev_url, self.root_dir(), entry_fixed)
645 scm.status(self._options, [], file_list)
646 modified_files = file_list != []
msb@chromium.org83017012009-09-28 18:52:12 +0000647 if not self._options.delete_unversioned_trees or modified_files:
maruel@chromium.orgc5e9aec2009-08-03 18:25:56 +0000648 # There are modified files in this entry. Keep warning until
649 # removed.
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000650 print(('\nWARNING: \'%s\' is no longer part of this client. '
651 'It is recommended that you manually remove it.\n') %
maruel@chromium.orgc5e9aec2009-08-03 18:25:56 +0000652 entry_fixed)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000653 else:
654 # Delete the entry
maruel@chromium.org73e21142010-07-05 13:32:01 +0000655 print('\n________ deleting \'%s\' in \'%s\'' % (
656 entry_fixed, self.root_dir()))
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000657 gclient_utils.RemoveDirectory(e_dir)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000658 # record the current list of entries for next time
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000659 self._SaveEntries()
maruel@chromium.org17cdf762010-05-28 17:30:52 +0000660 return 0
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000661
662 def PrintRevInfo(self):
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000663 """Output revision info mapping for the client and its dependencies.
664
665 This allows the capture of an overall "revision" for the source tree that
maruel@chromium.org918a9ae2010-05-28 15:50:30 +0000666 can be used to reproduce the same tree in the future. It is only useful for
667 "unpinned dependencies", i.e. DEPS/deps references without a svn revision
668 number or a git hash. A git branch name isn't "pinned" since the actual
669 commit can change.
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000670
671 The --snapshot option allows creating a .gclient file to reproduce the tree.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000672 """
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000673 if not self.dependencies:
maruel@chromium.org73e21142010-07-05 13:32:01 +0000674 raise gclient_utils.Error('No solution specified')
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000675 # Load all the settings.
676 self.RunCommandRecursively(self._options, {}, None, [], None)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000677
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000678 def GetURLAndRev(name, original_url):
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000679 """Returns the revision-qualified SCM url."""
680 if original_url is None:
maruel@chromium.orgbaa578e2010-07-12 17:36:59 +0000681 return None
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000682 if isinstance(original_url, self.FileImpl):
683 original_url = original_url.file_location
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000684 url, _ = gclient_utils.SplitUrlRevision(original_url)
maruel@chromium.org75a59272010-06-11 22:34:03 +0000685 scm = gclient_scm.CreateSCM(original_url, self.root_dir(), name)
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000686 if not os.path.isdir(scm.checkout_path):
687 return None
maruel@chromium.orgbaa578e2010-07-12 17:36:59 +0000688 return '%s@%s' % (url, scm.revinfo(self._options, [], None))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000689
maruel@chromium.orgbaa578e2010-07-12 17:36:59 +0000690 if self._options.snapshot:
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000691 new_gclient = ''
692 # First level at .gclient
693 for d in self.dependencies:
694 entries = {}
695 def GrabDeps(sol):
696 """Recursively grab dependencies."""
697 for i in sol.dependencies:
698 entries[i.name] = GetURLAndRev(i.name, i.parsed_url)
699 GrabDeps(i)
700 GrabDeps(d)
701 custom_deps = []
702 for k in sorted(entries.keys()):
703 if entries[k]:
704 # Quotes aren't escaped...
705 custom_deps.append(' \"%s\": \'%s\',\n' % (k, entries[k]))
706 else:
707 custom_deps.append(' \"%s\": None,\n' % k)
708 new_gclient += self.DEFAULT_SNAPSHOT_SOLUTION_TEXT % {
709 'solution_name': d.name,
710 'solution_url': d.url,
711 'safesync_url' : d.safesync_url or '',
712 'solution_deps': ''.join(custom_deps),
713 }
714 # Print the snapshot configuration file
715 print(self.DEFAULT_SNAPSHOT_FILE_TEXT % {'solution_list': new_gclient})
nasser@codeaurora.orgde8f3522010-03-11 23:47:44 +0000716 else:
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000717 entries = sorted(self.tree(False), key=lambda i: i.name)
718 for entry in entries:
719 entry_url = GetURLAndRev(entry.name, entry.parsed_url)
720 line = '%s: %s' % (entry.name, entry_url)
721 if not entry is entries[-1]:
722 line += ';'
723 print line
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000724
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000725 def ParseDepsFile(self, direct_reference):
726 """No DEPS to parse for a .gclient file."""
727 self.direct_reference = direct_reference
728 self.deps_parsed = True
729
maruel@chromium.org75a59272010-06-11 22:34:03 +0000730 def root_dir(self):
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000731 """Root directory of gclient checkout."""
maruel@chromium.org75a59272010-06-11 22:34:03 +0000732 return self._root_dir
733
maruel@chromium.org271375b2010-06-23 19:17:38 +0000734 def enforced_os(self):
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000735 """What deps_os entries that are to be parsed."""
maruel@chromium.org271375b2010-06-23 19:17:38 +0000736 return self._enforced_os
737
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000738 def recursion_limit(self):
739 """How recursive can each dependencies in DEPS file can load DEPS file."""
740 return self._recursion_limit
741
742 def tree(self, force_all):
743 """Returns a flat list of all the dependencies."""
744 def subtree(dep):
745 if not force_all and not dep.direct_reference:
746 # Was loaded from a From() keyword in a DEPS file, don't load all its
747 # dependencies.
748 return []
749 result = dep.dependencies[:]
750 for d in dep.dependencies:
751 result.extend(subtree(d))
752 return result
753 return subtree(self)
754
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000755
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000756#### gclient commands.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000757
758
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000759def CMDcleanup(parser, args):
760 """Cleans up all working copies.
maruel@chromium.orgddff62d2010-05-17 21:02:36 +0000761
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000762Mostly svn-specific. Simply runs 'svn cleanup' for each module.
maruel@chromium.org79692d62010-05-14 18:57:13 +0000763"""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000764 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
765 help='override deps for the specified (comma-separated) '
766 'platform(s); \'all\' will process all deps_os '
767 'references')
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000768 (options, args) = parser.parse_args(args)
maruel@chromium.org2806acc2009-05-15 12:33:34 +0000769 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000770 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000771 raise gclient_utils.Error('client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000772 if options.verbose:
773 # Print out the .gclient file. This is longer than if we just printed the
774 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.org116704f2010-06-11 17:34:38 +0000775 print(client.config_content)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000776 return client.RunOnDeps('cleanup', args)
777
778
piman@chromium.org4b90e3a2010-07-01 20:28:26 +0000779@attr('usage', '[command] [args ...]')
780def CMDrecurse(parser, args):
781 """Operates on all the entries.
782
783 Runs a shell command on all entries.
784 """
785 # Stop parsing at the first non-arg so that these go through to the command
786 parser.disable_interspersed_args()
787 parser.add_option('-s', '--scm', action='append', default=[],
788 help='choose scm types to operate upon')
789 options, args = parser.parse_args(args)
790 root, entries = gclient_utils.GetGClientRootAndEntries()
791 scm_set = set()
792 for scm in options.scm:
793 scm_set.update(scm.split(','))
794
795 # Pass in the SCM type as an env variable
796 env = os.environ.copy()
797
798 for path, url in entries.iteritems():
799 scm = gclient_scm.GetScmName(url)
800 if scm_set and scm not in scm_set:
801 continue
802 dir = os.path.normpath(os.path.join(root, path))
803 env['GCLIENT_SCM'] = scm
804 env['GCLIENT_URL'] = url
805 subprocess.Popen(args, cwd=dir, env=env).communicate()
806
807
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000808@attr('usage', '[url] [safesync url]')
809def CMDconfig(parser, args):
maruel@chromium.orgddff62d2010-05-17 21:02:36 +0000810 """Create a .gclient file in the current directory.
811
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000812This specifies the configuration for further commands. After update/sync,
maruel@chromium.org79692d62010-05-14 18:57:13 +0000813top-level DEPS files in each module are read to determine dependent
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000814modules to operate on as well. If optional [url] parameter is
maruel@chromium.org79692d62010-05-14 18:57:13 +0000815provided, then configuration is read from a specified Subversion server
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000816URL.
maruel@chromium.org79692d62010-05-14 18:57:13 +0000817"""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000818 parser.add_option('--spec',
819 help='create a gclient file containing the provided '
820 'string. Due to Cygwin/Python brokenness, it '
821 'probably can\'t contain any newlines.')
822 parser.add_option('--name',
823 help='overrides the default name for the solution')
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000824 (options, args) = parser.parse_args(args)
maruel@chromium.org5fc2a332010-05-26 19:37:15 +0000825 if ((options.spec and args) or len(args) > 2 or
826 (not options.spec and not args)):
827 parser.error('Inconsistent arguments. Use either --spec or one or 2 args')
828
maruel@chromium.org0329e672009-05-13 18:41:04 +0000829 if os.path.exists(options.config_filename):
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000830 raise gclient_utils.Error('%s file already exists in the current directory'
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000831 % options.config_filename)
maruel@chromium.org2806acc2009-05-15 12:33:34 +0000832 client = GClient('.', options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000833 if options.spec:
834 client.SetConfig(options.spec)
835 else:
maruel@chromium.org1ab7ffc2009-06-03 17:21:37 +0000836 base_url = args[0].rstrip('/')
iposva@chromium.org8cf7a392010-04-07 17:20:26 +0000837 if not options.name:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000838 name = base_url.split('/')[-1]
iposva@chromium.org8cf7a392010-04-07 17:20:26 +0000839 else:
840 # specify an alternate relpath for the given URL.
841 name = options.name
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000842 safesync_url = ''
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000843 if len(args) > 1:
844 safesync_url = args[1]
845 client.SetDefaultConfig(name, base_url, safesync_url)
846 client.SaveConfig()
maruel@chromium.org79692d62010-05-14 18:57:13 +0000847 return 0
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000848
849
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000850def CMDexport(parser, args):
maruel@chromium.orgddff62d2010-05-17 21:02:36 +0000851 """Wrapper for svn export for all managed directories."""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000852 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
853 help='override deps for the specified (comma-separated) '
854 'platform(s); \'all\' will process all deps_os '
855 'references')
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000856 (options, args) = parser.parse_args(args)
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +0000857 if len(args) != 1:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000858 raise gclient_utils.Error('Need directory name')
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +0000859 client = GClient.LoadCurrentConfig(options)
860
861 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000862 raise gclient_utils.Error('client not configured; see \'gclient config\'')
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +0000863
864 if options.verbose:
865 # Print out the .gclient file. This is longer than if we just printed the
866 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.org116704f2010-06-11 17:34:38 +0000867 print(client.config_content)
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +0000868 return client.RunOnDeps('export', args)
869
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000870
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000871@attr('epilog', """Example:
872 gclient pack > patch.txt
873 generate simple patch for configured client and dependences
874""")
875def CMDpack(parser, args):
maruel@chromium.org79692d62010-05-14 18:57:13 +0000876 """Generate a patch which can be applied at the root of the tree.
maruel@chromium.orgddff62d2010-05-17 21:02:36 +0000877
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000878Internally, runs 'svn diff'/'git diff' on each checked out module and
maruel@chromium.org79692d62010-05-14 18:57:13 +0000879dependencies, and performs minimal postprocessing of the output. The
880resulting patch is printed to stdout and can be applied to a freshly
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000881checked out tree via 'patch -p0 < patchfile'.
maruel@chromium.org79692d62010-05-14 18:57:13 +0000882"""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000883 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
884 help='override deps for the specified (comma-separated) '
885 'platform(s); \'all\' will process all deps_os '
886 'references')
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000887 (options, args) = parser.parse_args(args)
kbr@google.comab318592009-09-04 00:54:55 +0000888 client = GClient.LoadCurrentConfig(options)
889 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000890 raise gclient_utils.Error('client not configured; see \'gclient config\'')
kbr@google.comab318592009-09-04 00:54:55 +0000891 if options.verbose:
892 # Print out the .gclient file. This is longer than if we just printed the
893 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.org116704f2010-06-11 17:34:38 +0000894 print(client.config_content)
kbr@google.comab318592009-09-04 00:54:55 +0000895 return client.RunOnDeps('pack', args)
896
897
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000898def CMDstatus(parser, args):
899 """Show modification status for every dependencies."""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000900 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
901 help='override deps for the specified (comma-separated) '
902 'platform(s); \'all\' will process all deps_os '
903 'references')
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000904 (options, args) = parser.parse_args(args)
maruel@chromium.org2806acc2009-05-15 12:33:34 +0000905 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000906 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000907 raise gclient_utils.Error('client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000908 if options.verbose:
909 # Print out the .gclient file. This is longer than if we just printed the
910 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.org116704f2010-06-11 17:34:38 +0000911 print(client.config_content)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000912 return client.RunOnDeps('status', args)
913
914
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000915@attr('epilog', """Examples:
maruel@chromium.org79692d62010-05-14 18:57:13 +0000916 gclient sync
917 update files from SCM according to current configuration,
918 *for modules which have changed since last update or sync*
919 gclient sync --force
920 update files from SCM according to current configuration, for
921 all modules (useful for recovering files deleted from local copy)
922 gclient sync --revision src@31000
923 update src directory to r31000
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000924""")
925def CMDsync(parser, args):
926 """Checkout/update all modules."""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000927 parser.add_option('-f', '--force', action='store_true',
928 help='force update even for unchanged modules')
929 parser.add_option('-n', '--nohooks', action='store_true',
930 help='don\'t run hooks after the update is complete')
931 parser.add_option('-r', '--revision', action='append',
932 dest='revisions', metavar='REV', default=[],
933 help='Enforces revision/hash for the solutions with the '
934 'format src@rev. The src@ part is optional and can be '
935 'skipped. -r can be used multiple times when .gclient '
936 'has multiple solutions configured and will work even '
937 'if the src@ part is skipped.')
938 parser.add_option('-H', '--head', action='store_true',
939 help='skips any safesync_urls specified in '
940 'configured solutions and sync to head instead')
941 parser.add_option('-D', '--delete_unversioned_trees', action='store_true',
942 help='delete any unexpected unversioned trees '
943 'that are in the checkout')
944 parser.add_option('-R', '--reset', action='store_true',
945 help='resets any local changes before updating (git only)')
946 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
947 help='override deps for the specified (comma-separated) '
948 'platform(s); \'all\' will process all deps_os '
949 'references')
950 parser.add_option('-m', '--manually_grab_svn_rev', action='store_true',
951 help='Skip svn up whenever possible by requesting '
952 'actual HEAD revision from the repository')
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000953 (options, args) = parser.parse_args(args)
maruel@chromium.org2806acc2009-05-15 12:33:34 +0000954 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000955
956 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000957 raise gclient_utils.Error('client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000958
maruel@chromium.org307d1792010-05-31 20:03:13 +0000959 if options.revisions and options.head:
960 # TODO(maruel): Make it a parser.error if it doesn't break any builder.
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000961 print('Warning: you cannot use both --head and --revision')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000962
963 if options.verbose:
964 # Print out the .gclient file. This is longer than if we just printed the
965 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.org116704f2010-06-11 17:34:38 +0000966 print(client.config_content)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000967 return client.RunOnDeps('update', args)
968
969
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000970def CMDupdate(parser, args):
maruel@chromium.orgddff62d2010-05-17 21:02:36 +0000971 """Alias for the sync command. Deprecated."""
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000972 return CMDsync(parser, args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000973
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000974def CMDdiff(parser, args):
975 """Displays local diff for every dependencies."""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000976 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
977 help='override deps for the specified (comma-separated) '
978 'platform(s); \'all\' will process all deps_os '
979 'references')
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000980 (options, args) = parser.parse_args(args)
maruel@chromium.org2806acc2009-05-15 12:33:34 +0000981 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000982 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000983 raise gclient_utils.Error('client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000984 if options.verbose:
985 # Print out the .gclient file. This is longer than if we just printed the
986 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.org116704f2010-06-11 17:34:38 +0000987 print(client.config_content)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000988 return client.RunOnDeps('diff', args)
989
990
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000991def CMDrevert(parser, args):
992 """Revert all modifications in every dependencies."""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000993 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
994 help='override deps for the specified (comma-separated) '
995 'platform(s); \'all\' will process all deps_os '
996 'references')
997 parser.add_option('-n', '--nohooks', action='store_true',
998 help='don\'t run hooks after the revert is complete')
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000999 (options, args) = parser.parse_args(args)
1000 # --force is implied.
1001 options.force = True
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001002 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001003 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00001004 raise gclient_utils.Error('client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001005 return client.RunOnDeps('revert', args)
1006
1007
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001008def CMDrunhooks(parser, args):
1009 """Runs hooks for files that have been modified in the local working copy."""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00001010 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
1011 help='override deps for the specified (comma-separated) '
1012 'platform(s); \'all\' will process all deps_os '
1013 'references')
1014 parser.add_option('-f', '--force', action='store_true', default=True,
1015 help='Deprecated. No effect.')
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001016 (options, args) = parser.parse_args(args)
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001017 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001018 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00001019 raise gclient_utils.Error('client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001020 if options.verbose:
1021 # Print out the .gclient file. This is longer than if we just printed the
1022 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.org116704f2010-06-11 17:34:38 +00001023 print(client.config_content)
maruel@chromium.org5df6a462009-08-28 18:52:26 +00001024 options.force = True
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001025 options.nohooks = False
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001026 return client.RunOnDeps('runhooks', args)
1027
1028
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001029def CMDrevinfo(parser, args):
maruel@chromium.org9eda4112010-06-11 18:56:10 +00001030 """Output revision info mapping for the client and its dependencies.
1031
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00001032 This allows the capture of an overall 'revision' for the source tree that
maruel@chromium.org9eda4112010-06-11 18:56:10 +00001033 can be used to reproduce the same tree in the future. It is only useful for
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00001034 'unpinned dependencies', i.e. DEPS/deps references without a svn revision
1035 number or a git hash. A git branch name isn't 'pinned' since the actual
maruel@chromium.org9eda4112010-06-11 18:56:10 +00001036 commit can change.
1037 """
1038 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
1039 help='override deps for the specified (comma-separated) '
1040 'platform(s); \'all\' will process all deps_os '
1041 'references')
1042 parser.add_option('-s', '--snapshot', action='store_true',
1043 help='creates a snapshot .gclient file of the current '
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +00001044 'version of all repositories to reproduce the tree')
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001045 (options, args) = parser.parse_args(args)
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001046 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001047 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00001048 raise gclient_utils.Error('client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001049 client.PrintRevInfo()
maruel@chromium.org79692d62010-05-14 18:57:13 +00001050 return 0
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001051
1052
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001053def Command(name):
1054 return getattr(sys.modules[__name__], 'CMD' + name, None)
1055
1056
1057def CMDhelp(parser, args):
1058 """Prints list of commands or help for a specific command."""
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001059 (_, args) = parser.parse_args(args)
maruel@chromium.orgddff62d2010-05-17 21:02:36 +00001060 if len(args) == 1:
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001061 return Main(args + ['--help'])
maruel@chromium.orgddff62d2010-05-17 21:02:36 +00001062 parser.print_help()
1063 return 0
1064
1065
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001066def GenUsage(parser, command):
1067 """Modify an OptParse object with the function's documentation."""
1068 obj = Command(command)
1069 if command == 'help':
1070 command = '<command>'
1071 # OptParser.description prefer nicely non-formatted strings.
1072 parser.description = re.sub('[\r\n ]{2,}', ' ', obj.__doc__)
1073 usage = getattr(obj, 'usage', '')
1074 parser.set_usage('%%prog %s [options] %s' % (command, usage))
1075 parser.epilog = getattr(obj, 'epilog', None)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001076
1077
1078def Main(argv):
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001079 """Doesn't parse the arguments here, just find the right subcommand to
1080 execute."""
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001081 try:
1082 # Do it late so all commands are listed.
1083 CMDhelp.usage = ('\n\nCommands are:\n' + '\n'.join([
1084 ' %-10s %s' % (fn[3:], Command(fn[3:]).__doc__.split('\n')[0].strip())
1085 for fn in dir(sys.modules[__name__]) if fn.startswith('CMD')]))
1086 parser = optparse.OptionParser(version='%prog ' + __version__)
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001087 parser.add_option('-v', '--verbose', action='count', default=0,
1088 help='Produces additional output for diagnostics. Can be '
1089 'used up to three times for more logging info.')
1090 parser.add_option('--gclientfile', dest='config_filename',
1091 default=os.environ.get('GCLIENT_FILE', '.gclient'),
1092 help='Specify an alternate %default file')
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001093 # Integrate standard options processing.
1094 old_parser = parser.parse_args
1095 def Parse(args):
1096 (options, args) = old_parser(args)
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001097 level = None
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001098 if options.verbose == 2:
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001099 level = logging.INFO
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001100 elif options.verbose > 2:
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001101 level = logging.DEBUG
1102 logging.basicConfig(level=level,
1103 format='%(module)s(%(lineno)d) %(funcName)s:%(message)s')
1104 options.entries_filename = options.config_filename + '_entries'
maruel@chromium.orge3216c62010-07-08 03:31:43 +00001105
1106 # These hacks need to die.
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001107 if not hasattr(options, 'revisions'):
1108 # GClient.RunOnDeps expects it even if not applicable.
1109 options.revisions = []
1110 if not hasattr(options, 'head'):
1111 options.head = None
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001112 if not hasattr(options, 'nohooks'):
1113 options.nohooks = True
1114 if not hasattr(options, 'deps_os'):
1115 options.deps_os = None
maruel@chromium.orge3216c62010-07-08 03:31:43 +00001116 if not hasattr(options, 'manually_grab_svn_rev'):
1117 options.manually_grab_svn_rev = None
1118 if not hasattr(options, 'force'):
1119 options.force = None
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001120 return (options, args)
1121 parser.parse_args = Parse
1122 # We don't want wordwrapping in epilog (usually examples)
1123 parser.format_epilog = lambda _: parser.epilog or ''
1124 if argv:
1125 command = Command(argv[0])
1126 if command:
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001127 # 'fix' the usage and the description now that we know the subcommand.
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001128 GenUsage(parser, argv[0])
1129 return command(parser, argv[1:])
1130 # Not a known command. Default to help.
1131 GenUsage(parser, 'help')
1132 return CMDhelp(parser, argv)
1133 except gclient_utils.Error, e:
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001134 print >> sys.stderr, 'Error: %s' % str(e)
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001135 return 1
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001136
1137
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001138if '__main__' == __name__:
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001139 sys.exit(Main(sys.argv[1:]))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001140
1141# vim: ts=2:sw=2:tw=80:et: