blob: ff47969339bcc3756f42def901f4694a87b93002 [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.org917aa912010-07-09 20:35:18 +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.org917aa912010-07-09 20:35:18 +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.org917aa912010-07-09 20:35:18 +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.org917aa912010-07-09 20:35:18 +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.org917aa912010-07-09 20:35:18 +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.org917aa912010-07-09 20:35:18 +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.org917aa912010-07-09 20:35:18 +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.org917aa912010-07-09 20:35:18 +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.org917aa912010-07-09 20:35:18 +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.org917aa912010-07-09 20:35:18 +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.org5248d9d2010-07-07 20:03:51 +0000344 # If "--force" was specified, run all hooks regardless of what files have
maruel@chromium.org917aa912010-07-09 20:35:18 +0000345 # changed.
346 if self.deps_hooks:
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 gclient_scm.GetScmName(self.parsed_url) in ('git', None) or
352 os.path.isdir(os.path.join(self.root_dir(), self.name, '.git'))):
353 for hook_dict in self.deps_hooks:
354 self._RunHookAction(hook_dict, [])
355 else:
356 # TODO(phajdan.jr): We should know exactly when the paths are absolute.
357 # Convert all absolute paths to relative.
358 for i in range(len(self.file_list)):
359 # It depends on the command being executed (like runhooks vs sync).
360 if not os.path.isabs(self.file_list[i]):
361 continue
maruel@chromium.org5248d9d2010-07-07 20:03:51 +0000362
maruel@chromium.org917aa912010-07-09 20:35:18 +0000363 prefix = os.path.commonprefix([self.root_dir().lower(),
364 self.file_list[i].lower()])
365 self.file_list[i] = self.file_list[i][len(prefix):]
366
367 # Strip any leading path separators.
368 while (self.file_list[i].startswith('\\') or
369 self.file_list[i].startswith('/')):
370 self.file_list[i] = self.file_list[i][1:]
371
372 # Run hooks on the basis of whether the files from the gclient operation
373 # match each hook's pattern.
374 for hook_dict in self.deps_hooks:
375 pattern = re.compile(hook_dict['pattern'])
376 matching_file_list = [f for f in self.file_list if pattern.search(f)]
377 if matching_file_list:
378 self._RunHookAction(hook_dict, matching_file_list)
379 if self.recursion_limit():
380 for s in self.dependencies:
381 s.RunHooksRecursively(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000382
maruel@chromium.orgeaf61062010-07-07 18:42:39 +0000383 def _RunHookAction(self, hook_dict, matching_file_list):
384 """Runs the action from a single hook."""
385 logging.info(hook_dict)
386 logging.info(matching_file_list)
387 command = hook_dict['action'][:]
388 if command[0] == 'python':
389 # If the hook specified "python" as the first item, the action is a
390 # Python script. Run it by starting a new copy of the same
391 # interpreter.
392 command[0] = sys.executable
393
394 if '$matching_files' in command:
395 splice_index = command.index('$matching_files')
396 command[splice_index:splice_index + 1] = matching_file_list
397
398 # Use a discrete exit status code of 2 to indicate that a hook action
399 # failed. Users of this script may wish to treat hook action failures
400 # differently from VC failures.
401 return gclient_utils.SubprocessCall(command, self.root_dir(), fail_status=2)
402
maruel@chromium.org271375b2010-06-23 19:17:38 +0000403 def root_dir(self):
404 return self.parent.root_dir()
405
406 def enforced_os(self):
407 return self.parent.enforced_os()
408
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000409 def recursion_limit(self):
410 return self.parent.recursion_limit() - 1
411
412 def tree(self, force_all):
413 return self.parent.tree(force_all)
414
415 def get_custom_deps(self, name, url):
416 """Returns a custom deps if applicable."""
417 if self.parent:
418 url = self.parent.get_custom_deps(name, url)
419 # None is a valid return value to disable a dependency.
420 return self.custom_deps.get(name, url)
421
422 def __str__(self):
423 out = []
424 for i in ('name', 'url', 'safesync_url', 'custom_deps', 'custom_vars',
maruel@chromium.org917aa912010-07-09 20:35:18 +0000425 'deps_hooks', 'file_list'):
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000426 # 'deps_file'
427 if self.__dict__[i]:
428 out.append('%s: %s' % (i, self.__dict__[i]))
429
430 for d in self.dependencies:
431 out.extend([' ' + x for x in str(d).splitlines()])
432 out.append('')
433 return '\n'.join(out)
434
435 def __repr__(self):
436 return '%s: %s' % (self.name, self.url)
437
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000438
439class GClient(Dependency):
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000440 """Object that represent a gclient checkout. A tree of Dependency(), one per
441 solution or DEPS entry."""
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000442
443 DEPS_OS_CHOICES = {
444 "win32": "win",
445 "win": "win",
446 "cygwin": "win",
447 "darwin": "mac",
448 "mac": "mac",
449 "unix": "unix",
450 "linux": "unix",
451 "linux2": "unix",
452 }
453
454 DEFAULT_CLIENT_FILE_TEXT = ("""\
455solutions = [
456 { "name" : "%(solution_name)s",
457 "url" : "%(solution_url)s",
458 "custom_deps" : {
459 },
maruel@chromium.org73e21142010-07-05 13:32:01 +0000460 "safesync_url": "%(safesync_url)s",
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000461 },
462]
463""")
464
465 DEFAULT_SNAPSHOT_SOLUTION_TEXT = ("""\
466 { "name" : "%(solution_name)s",
467 "url" : "%(solution_url)s",
468 "custom_deps" : {
maruel@chromium.org73e21142010-07-05 13:32:01 +0000469%(solution_deps)s },
470 "safesync_url": "%(safesync_url)s",
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000471 },
472""")
473
474 DEFAULT_SNAPSHOT_FILE_TEXT = ("""\
475# Snapshot generated with gclient revinfo --snapshot
476solutions = [
maruel@chromium.org73e21142010-07-05 13:32:01 +0000477%(solution_list)s]
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000478""")
479
480 def __init__(self, root_dir, options):
481 Dependency.__init__(self, None, None, None)
maruel@chromium.org0d425922010-06-21 19:22:24 +0000482 self._options = options
maruel@chromium.org271375b2010-06-23 19:17:38 +0000483 if options.deps_os:
484 enforced_os = options.deps_os.split(',')
485 else:
486 enforced_os = [self.DEPS_OS_CHOICES.get(sys.platform, 'unix')]
487 if 'all' in enforced_os:
488 enforced_os = self.DEPS_OS_CHOICES.itervalues()
489 self._enforced_os = list(set(enforced_os))
490 self._root_dir = root_dir
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000491 self.config_content = None
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000492 # Do not change previous behavior. Only solution level and immediate DEPS
493 # are processed.
494 self._recursion_limit = 2
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000495
496 def SetConfig(self, content):
497 assert self.dependencies == []
498 config_dict = {}
499 self.config_content = content
500 try:
501 exec(content, config_dict)
502 except SyntaxError, e:
maruel@chromium.org5990f9d2010-07-07 18:02:58 +0000503 gclient_utils.SyntaxErrorToError('.gclient', e)
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000504 for s in config_dict.get('solutions', []):
maruel@chromium.org81843b82010-06-28 16:49:26 +0000505 try:
506 self.dependencies.append(Dependency(
507 self, s['name'], s['url'],
508 s.get('safesync_url', None),
509 s.get('custom_deps', {}),
510 s.get('custom_vars', {})))
511 except KeyError:
512 raise gclient_utils.Error('Invalid .gclient file. Solution is '
513 'incomplete: %s' % s)
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000514 # .gclient can have hooks.
515 self.deps_hooks = config_dict.get('hooks', [])
516
517 def SaveConfig(self):
518 gclient_utils.FileWrite(os.path.join(self.root_dir(),
519 self._options.config_filename),
520 self.config_content)
521
522 @staticmethod
523 def LoadCurrentConfig(options):
524 """Searches for and loads a .gclient file relative to the current working
525 dir. Returns a GClient object."""
526 path = gclient_utils.FindGclientRoot(os.getcwd(), options.config_filename)
527 if not path:
528 return None
529 client = GClient(path, options)
530 client.SetConfig(gclient_utils.FileRead(
531 os.path.join(path, options.config_filename)))
532 return client
533
534 def SetDefaultConfig(self, solution_name, solution_url, safesync_url):
535 self.SetConfig(self.DEFAULT_CLIENT_FILE_TEXT % {
536 'solution_name': solution_name,
537 'solution_url': solution_url,
538 'safesync_url' : safesync_url,
539 })
540
maruel@chromium.org917aa912010-07-09 20:35:18 +0000541 def _SaveEntries(self):
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000542 """Creates a .gclient_entries file to record the list of unique checkouts.
543
544 The .gclient_entries file lives in the same directory as .gclient.
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000545 """
546 # Sometimes pprint.pformat will use {', sometimes it'll use { ' ... It
547 # makes testing a bit too fun.
maruel@chromium.org917aa912010-07-09 20:35:18 +0000548 result = 'entries = {\n'
549 for entry in self.tree(False):
550 # Skip over File() dependencies as we can't version them.
551 if not isinstance(entry.parsed_url, self.FileImpl):
552 result += ' %s: %s,\n' % (pprint.pformat(entry.name),
553 pprint.pformat(entry.parsed_url))
554 result += '}\n'
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000555 file_path = os.path.join(self.root_dir(), self._options.entries_filename)
maruel@chromium.org917aa912010-07-09 20:35:18 +0000556 logging.info(result)
557 gclient_utils.FileWrite(file_path, result)
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000558
559 def _ReadEntries(self):
560 """Read the .gclient_entries file for the given client.
561
562 Returns:
563 A sequence of solution names, which will be empty if there is the
564 entries file hasn't been created yet.
565 """
566 scope = {}
567 filename = os.path.join(self.root_dir(), self._options.entries_filename)
568 if not os.path.exists(filename):
maruel@chromium.org73e21142010-07-05 13:32:01 +0000569 return {}
maruel@chromium.org5990f9d2010-07-07 18:02:58 +0000570 try:
571 exec(gclient_utils.FileRead(filename), scope)
572 except SyntaxError, e:
573 gclient_utils.SyntaxErrorToError(filename, e)
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000574 return scope['entries']
575
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000576 def _EnforceRevisions(self):
maruel@chromium.org918a9ae2010-05-28 15:50:30 +0000577 """Checks for revision overrides."""
578 revision_overrides = {}
maruel@chromium.org307d1792010-05-31 20:03:13 +0000579 if self._options.head:
580 return revision_overrides
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000581 for s in self.dependencies:
582 if not s.safesync_url:
maruel@chromium.org307d1792010-05-31 20:03:13 +0000583 continue
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000584 handle = urllib.urlopen(s.safesync_url)
maruel@chromium.org307d1792010-05-31 20:03:13 +0000585 rev = handle.read().strip()
586 handle.close()
587 if len(rev):
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000588 self._options.revisions.append('%s@%s' % (s.name, rev))
maruel@chromium.org307d1792010-05-31 20:03:13 +0000589 if not self._options.revisions:
590 return revision_overrides
591 # --revision will take over safesync_url.
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000592 solutions_names = [s.name for s in self.dependencies]
maruel@chromium.org307d1792010-05-31 20:03:13 +0000593 index = 0
594 for revision in self._options.revisions:
595 if not '@' in revision:
596 # Support for --revision 123
597 revision = '%s@%s' % (solutions_names[index], revision)
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000598 sol, rev = revision.split('@', 1)
maruel@chromium.org307d1792010-05-31 20:03:13 +0000599 if not sol in solutions_names:
600 #raise gclient_utils.Error('%s is not a valid solution.' % sol)
601 print >> sys.stderr, ('Please fix your script, having invalid '
602 '--revision flags will soon considered an error.')
603 else:
maruel@chromium.org918a9ae2010-05-28 15:50:30 +0000604 revision_overrides[sol] = rev
maruel@chromium.org307d1792010-05-31 20:03:13 +0000605 index += 1
maruel@chromium.org918a9ae2010-05-28 15:50:30 +0000606 return revision_overrides
607
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000608 def RunOnDeps(self, command, args):
609 """Runs a command on each dependency in a client and its dependencies.
610
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000611 Args:
612 command: The command to use (e.g., 'status' or 'diff')
613 args: list of str - extra arguments to add to the command line.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000614 """
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000615 if not self.dependencies:
maruel@chromium.org73e21142010-07-05 13:32:01 +0000616 raise gclient_utils.Error('No solution specified')
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000617 revision_overrides = self._EnforceRevisions()
maruel@chromium.org917aa912010-07-09 20:35:18 +0000618 pm = None
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000619 if command == 'update' and not self._options.verbose:
maruel@chromium.org917aa912010-07-09 20:35:18 +0000620 pm = Progress('Syncing projects', len(self.tree(False)) + 1)
621 self.RunCommandRecursively(self._options, revision_overrides,
622 command, args, pm)
623 if pm:
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000624 pm.end()
piman@chromium.org6f363722010-04-27 00:41:09 +0000625
maruel@chromium.org917aa912010-07-09 20:35:18 +0000626 # Once all the dependencies have been processed, it's now safe to run the
627 # hooks.
628 if not self._options.nohooks:
629 self.RunHooksRecursively(self._options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000630
631 if command == 'update':
ajwong@chromium.orgcdcee802009-06-23 15:30:42 +0000632 # Notify the user if there is an orphaned entry in their working copy.
633 # Only delete the directory if there are no changes in it, and
634 # delete_unversioned_trees is set to true.
maruel@chromium.org917aa912010-07-09 20:35:18 +0000635 entries = [i.name for i in self.tree(False)]
636 for entry, prev_url in self._ReadEntries().iteritems():
maruel@chromium.orgc5e9aec2009-08-03 18:25:56 +0000637 # Fix path separator on Windows.
638 entry_fixed = entry.replace('/', os.path.sep)
maruel@chromium.org75a59272010-06-11 22:34:03 +0000639 e_dir = os.path.join(self.root_dir(), entry_fixed)
maruel@chromium.orgc5e9aec2009-08-03 18:25:56 +0000640 # Use entry and not entry_fixed there.
maruel@chromium.org0329e672009-05-13 18:41:04 +0000641 if entry not in entries and os.path.exists(e_dir):
maruel@chromium.org917aa912010-07-09 20:35:18 +0000642 file_list = []
643 scm = gclient_scm.CreateSCM(prev_url, self.root_dir(), entry_fixed)
644 scm.status(self._options, [], file_list)
645 modified_files = file_list != []
msb@chromium.org83017012009-09-28 18:52:12 +0000646 if not self._options.delete_unversioned_trees or modified_files:
maruel@chromium.orgc5e9aec2009-08-03 18:25:56 +0000647 # There are modified files in this entry. Keep warning until
648 # removed.
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000649 print(('\nWARNING: \'%s\' is no longer part of this client. '
650 'It is recommended that you manually remove it.\n') %
maruel@chromium.orgc5e9aec2009-08-03 18:25:56 +0000651 entry_fixed)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000652 else:
653 # Delete the entry
maruel@chromium.org73e21142010-07-05 13:32:01 +0000654 print('\n________ deleting \'%s\' in \'%s\'' % (
655 entry_fixed, self.root_dir()))
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000656 gclient_utils.RemoveDirectory(e_dir)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000657 # record the current list of entries for next time
maruel@chromium.org917aa912010-07-09 20:35:18 +0000658 self._SaveEntries()
maruel@chromium.org17cdf762010-05-28 17:30:52 +0000659 return 0
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000660
661 def PrintRevInfo(self):
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000662 """Output revision info mapping for the client and its dependencies.
663
664 This allows the capture of an overall "revision" for the source tree that
maruel@chromium.org918a9ae2010-05-28 15:50:30 +0000665 can be used to reproduce the same tree in the future. It is only useful for
666 "unpinned dependencies", i.e. DEPS/deps references without a svn revision
667 number or a git hash. A git branch name isn't "pinned" since the actual
668 commit can change.
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000669
670 The --snapshot option allows creating a .gclient file to reproduce the tree.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000671 """
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000672 if not self.dependencies:
maruel@chromium.org73e21142010-07-05 13:32:01 +0000673 raise gclient_utils.Error('No solution specified')
maruel@chromium.org917aa912010-07-09 20:35:18 +0000674 # Load all the settings.
675 self.RunCommandRecursively(self._options, {}, None, [], None)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000676
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000677 def GetURLAndRev(name, original_url):
maruel@chromium.org917aa912010-07-09 20:35:18 +0000678 """Returns the revision-qualified SCM url."""
679 if isinstance(original_url, self.FileImpl):
680 return original_url.file_location
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000681 url, _ = gclient_utils.SplitUrlRevision(original_url)
maruel@chromium.org75a59272010-06-11 22:34:03 +0000682 scm = gclient_scm.CreateSCM(original_url, self.root_dir(), name)
maruel@chromium.org917aa912010-07-09 20:35:18 +0000683 if not os.path.isdir(scm.checkout_path):
684 return None
685 return '%s@%s' % (url, scm.revinfo(self._options, [], None))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000686
nasser@codeaurora.orgde8f3522010-03-11 23:47:44 +0000687 if self._options.snapshot:
maruel@chromium.org917aa912010-07-09 20:35:18 +0000688 new_gclient = ''
689 # First level at .gclient
690 for d in self.dependencies:
691 entries = {}
692 def GrabDeps(sol):
693 """Recursively grab dependencies."""
694 for i in sol.dependencies:
695 entries[i.name] = GetURLAndRev(i.name, i.parsed_url)
696 GrabDeps(i)
697 GrabDeps(d)
698 custom_deps = []
699 for k in sorted(entries.keys()):
700 if entries[k]:
701 # Quotes aren't escaped...
702 custom_deps.append(' \"%s\": \'%s\',\n' % (k, entries[k]))
703 else:
704 custom_deps.append(' \"%s\": None,\n' % k)
705 new_gclient += self.DEFAULT_SNAPSHOT_SOLUTION_TEXT % {
706 'solution_name': d.name,
707 'solution_url': d.url,
708 'safesync_url' : d.safesync_url or '',
709 'solution_deps': ''.join(custom_deps),
710 }
711 # Print the snapshot configuration file
712 print(self.DEFAULT_SNAPSHOT_FILE_TEXT % {'solution_list': new_gclient})
nasser@codeaurora.orgde8f3522010-03-11 23:47:44 +0000713 else:
maruel@chromium.org917aa912010-07-09 20:35:18 +0000714 entries = sorted(self.tree(False), key=lambda i: i.name)
715 for entry in entries:
716 revision = GetURLAndRev(entry.name, entry.parsed_url)
717 line = '%s: %s' % (entry.name, revision)
718 if not entry is entries[-1]:
719 line += ';'
720 print line
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000721
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000722 def ParseDepsFile(self, direct_reference):
723 """No DEPS to parse for a .gclient file."""
724 self.direct_reference = direct_reference
725 self.deps_parsed = True
726
maruel@chromium.org75a59272010-06-11 22:34:03 +0000727 def root_dir(self):
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000728 """Root directory of gclient checkout."""
maruel@chromium.org75a59272010-06-11 22:34:03 +0000729 return self._root_dir
730
maruel@chromium.org271375b2010-06-23 19:17:38 +0000731 def enforced_os(self):
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000732 """What deps_os entries that are to be parsed."""
maruel@chromium.org271375b2010-06-23 19:17:38 +0000733 return self._enforced_os
734
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000735 def recursion_limit(self):
736 """How recursive can each dependencies in DEPS file can load DEPS file."""
737 return self._recursion_limit
738
739 def tree(self, force_all):
740 """Returns a flat list of all the dependencies."""
741 def subtree(dep):
742 if not force_all and not dep.direct_reference:
743 # Was loaded from a From() keyword in a DEPS file, don't load all its
744 # dependencies.
745 return []
746 result = dep.dependencies[:]
747 for d in dep.dependencies:
748 result.extend(subtree(d))
749 return result
750 return subtree(self)
751
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000752
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000753#### gclient commands.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000754
755
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000756def CMDcleanup(parser, args):
757 """Cleans up all working copies.
maruel@chromium.orgddff62d2010-05-17 21:02:36 +0000758
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000759Mostly svn-specific. Simply runs 'svn cleanup' for each module.
maruel@chromium.org79692d62010-05-14 18:57:13 +0000760"""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000761 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
762 help='override deps for the specified (comma-separated) '
763 'platform(s); \'all\' will process all deps_os '
764 'references')
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000765 (options, args) = parser.parse_args(args)
maruel@chromium.org2806acc2009-05-15 12:33:34 +0000766 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000767 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000768 raise gclient_utils.Error('client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000769 if options.verbose:
770 # Print out the .gclient file. This is longer than if we just printed the
771 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.org116704f2010-06-11 17:34:38 +0000772 print(client.config_content)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000773 return client.RunOnDeps('cleanup', args)
774
775
piman@chromium.org4b90e3a2010-07-01 20:28:26 +0000776@attr('usage', '[command] [args ...]')
777def CMDrecurse(parser, args):
778 """Operates on all the entries.
779
780 Runs a shell command on all entries.
781 """
782 # Stop parsing at the first non-arg so that these go through to the command
783 parser.disable_interspersed_args()
784 parser.add_option('-s', '--scm', action='append', default=[],
785 help='choose scm types to operate upon')
786 options, args = parser.parse_args(args)
787 root, entries = gclient_utils.GetGClientRootAndEntries()
788 scm_set = set()
789 for scm in options.scm:
790 scm_set.update(scm.split(','))
791
792 # Pass in the SCM type as an env variable
793 env = os.environ.copy()
794
795 for path, url in entries.iteritems():
796 scm = gclient_scm.GetScmName(url)
797 if scm_set and scm not in scm_set:
798 continue
799 dir = os.path.normpath(os.path.join(root, path))
800 env['GCLIENT_SCM'] = scm
801 env['GCLIENT_URL'] = url
802 subprocess.Popen(args, cwd=dir, env=env).communicate()
803
804
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000805@attr('usage', '[url] [safesync url]')
806def CMDconfig(parser, args):
maruel@chromium.orgddff62d2010-05-17 21:02:36 +0000807 """Create a .gclient file in the current directory.
808
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000809This specifies the configuration for further commands. After update/sync,
maruel@chromium.org79692d62010-05-14 18:57:13 +0000810top-level DEPS files in each module are read to determine dependent
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000811modules to operate on as well. If optional [url] parameter is
maruel@chromium.org79692d62010-05-14 18:57:13 +0000812provided, then configuration is read from a specified Subversion server
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000813URL.
maruel@chromium.org79692d62010-05-14 18:57:13 +0000814"""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000815 parser.add_option('--spec',
816 help='create a gclient file containing the provided '
817 'string. Due to Cygwin/Python brokenness, it '
818 'probably can\'t contain any newlines.')
819 parser.add_option('--name',
820 help='overrides the default name for the solution')
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000821 (options, args) = parser.parse_args(args)
maruel@chromium.org5fc2a332010-05-26 19:37:15 +0000822 if ((options.spec and args) or len(args) > 2 or
823 (not options.spec and not args)):
824 parser.error('Inconsistent arguments. Use either --spec or one or 2 args')
825
maruel@chromium.org0329e672009-05-13 18:41:04 +0000826 if os.path.exists(options.config_filename):
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000827 raise gclient_utils.Error('%s file already exists in the current directory'
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000828 % options.config_filename)
maruel@chromium.org2806acc2009-05-15 12:33:34 +0000829 client = GClient('.', options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000830 if options.spec:
831 client.SetConfig(options.spec)
832 else:
maruel@chromium.org1ab7ffc2009-06-03 17:21:37 +0000833 base_url = args[0].rstrip('/')
iposva@chromium.org8cf7a392010-04-07 17:20:26 +0000834 if not options.name:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000835 name = base_url.split('/')[-1]
iposva@chromium.org8cf7a392010-04-07 17:20:26 +0000836 else:
837 # specify an alternate relpath for the given URL.
838 name = options.name
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000839 safesync_url = ''
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000840 if len(args) > 1:
841 safesync_url = args[1]
842 client.SetDefaultConfig(name, base_url, safesync_url)
843 client.SaveConfig()
maruel@chromium.org79692d62010-05-14 18:57:13 +0000844 return 0
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000845
846
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000847def CMDexport(parser, args):
maruel@chromium.orgddff62d2010-05-17 21:02:36 +0000848 """Wrapper for svn export for all managed directories."""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000849 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
850 help='override deps for the specified (comma-separated) '
851 'platform(s); \'all\' will process all deps_os '
852 'references')
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000853 (options, args) = parser.parse_args(args)
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +0000854 if len(args) != 1:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000855 raise gclient_utils.Error('Need directory name')
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +0000856 client = GClient.LoadCurrentConfig(options)
857
858 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000859 raise gclient_utils.Error('client not configured; see \'gclient config\'')
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +0000860
861 if options.verbose:
862 # Print out the .gclient file. This is longer than if we just printed the
863 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.org116704f2010-06-11 17:34:38 +0000864 print(client.config_content)
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +0000865 return client.RunOnDeps('export', args)
866
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000867
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000868@attr('epilog', """Example:
869 gclient pack > patch.txt
870 generate simple patch for configured client and dependences
871""")
872def CMDpack(parser, args):
maruel@chromium.org79692d62010-05-14 18:57:13 +0000873 """Generate a patch which can be applied at the root of the tree.
maruel@chromium.orgddff62d2010-05-17 21:02:36 +0000874
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000875Internally, runs 'svn diff'/'git diff' on each checked out module and
maruel@chromium.org79692d62010-05-14 18:57:13 +0000876dependencies, and performs minimal postprocessing of the output. The
877resulting patch is printed to stdout and can be applied to a freshly
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000878checked out tree via 'patch -p0 < patchfile'.
maruel@chromium.org79692d62010-05-14 18:57:13 +0000879"""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000880 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
881 help='override deps for the specified (comma-separated) '
882 'platform(s); \'all\' will process all deps_os '
883 'references')
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000884 (options, args) = parser.parse_args(args)
kbr@google.comab318592009-09-04 00:54:55 +0000885 client = GClient.LoadCurrentConfig(options)
886 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000887 raise gclient_utils.Error('client not configured; see \'gclient config\'')
kbr@google.comab318592009-09-04 00:54:55 +0000888 if options.verbose:
889 # Print out the .gclient file. This is longer than if we just printed the
890 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.org116704f2010-06-11 17:34:38 +0000891 print(client.config_content)
kbr@google.comab318592009-09-04 00:54:55 +0000892 return client.RunOnDeps('pack', args)
893
894
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000895def CMDstatus(parser, args):
896 """Show modification status for every dependencies."""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000897 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
898 help='override deps for the specified (comma-separated) '
899 'platform(s); \'all\' will process all deps_os '
900 'references')
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000901 (options, args) = parser.parse_args(args)
maruel@chromium.org2806acc2009-05-15 12:33:34 +0000902 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000903 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000904 raise gclient_utils.Error('client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000905 if options.verbose:
906 # Print out the .gclient file. This is longer than if we just printed the
907 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.org116704f2010-06-11 17:34:38 +0000908 print(client.config_content)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000909 return client.RunOnDeps('status', args)
910
911
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000912@attr('epilog', """Examples:
maruel@chromium.org79692d62010-05-14 18:57:13 +0000913 gclient sync
914 update files from SCM according to current configuration,
915 *for modules which have changed since last update or sync*
916 gclient sync --force
917 update files from SCM according to current configuration, for
918 all modules (useful for recovering files deleted from local copy)
919 gclient sync --revision src@31000
920 update src directory to r31000
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000921""")
922def CMDsync(parser, args):
923 """Checkout/update all modules."""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000924 parser.add_option('-f', '--force', action='store_true',
925 help='force update even for unchanged modules')
926 parser.add_option('-n', '--nohooks', action='store_true',
927 help='don\'t run hooks after the update is complete')
928 parser.add_option('-r', '--revision', action='append',
929 dest='revisions', metavar='REV', default=[],
930 help='Enforces revision/hash for the solutions with the '
931 'format src@rev. The src@ part is optional and can be '
932 'skipped. -r can be used multiple times when .gclient '
933 'has multiple solutions configured and will work even '
934 'if the src@ part is skipped.')
935 parser.add_option('-H', '--head', action='store_true',
936 help='skips any safesync_urls specified in '
937 'configured solutions and sync to head instead')
938 parser.add_option('-D', '--delete_unversioned_trees', action='store_true',
939 help='delete any unexpected unversioned trees '
940 'that are in the checkout')
941 parser.add_option('-R', '--reset', action='store_true',
942 help='resets any local changes before updating (git only)')
943 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
944 help='override deps for the specified (comma-separated) '
945 'platform(s); \'all\' will process all deps_os '
946 'references')
947 parser.add_option('-m', '--manually_grab_svn_rev', action='store_true',
948 help='Skip svn up whenever possible by requesting '
949 'actual HEAD revision from the repository')
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000950 (options, args) = parser.parse_args(args)
maruel@chromium.org2806acc2009-05-15 12:33:34 +0000951 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000952
953 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000954 raise gclient_utils.Error('client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000955
maruel@chromium.org307d1792010-05-31 20:03:13 +0000956 if options.revisions and options.head:
957 # TODO(maruel): Make it a parser.error if it doesn't break any builder.
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000958 print('Warning: you cannot use both --head and --revision')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000959
960 if options.verbose:
961 # Print out the .gclient file. This is longer than if we just printed the
962 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.org116704f2010-06-11 17:34:38 +0000963 print(client.config_content)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000964 return client.RunOnDeps('update', args)
965
966
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000967def CMDupdate(parser, args):
maruel@chromium.orgddff62d2010-05-17 21:02:36 +0000968 """Alias for the sync command. Deprecated."""
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000969 return CMDsync(parser, args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000970
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000971def CMDdiff(parser, args):
972 """Displays local diff for every dependencies."""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000973 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
974 help='override deps for the specified (comma-separated) '
975 'platform(s); \'all\' will process all deps_os '
976 'references')
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000977 (options, args) = parser.parse_args(args)
maruel@chromium.org2806acc2009-05-15 12:33:34 +0000978 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000979 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000980 raise gclient_utils.Error('client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000981 if options.verbose:
982 # Print out the .gclient file. This is longer than if we just printed the
983 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.org116704f2010-06-11 17:34:38 +0000984 print(client.config_content)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000985 return client.RunOnDeps('diff', args)
986
987
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000988def CMDrevert(parser, args):
989 """Revert all modifications in every dependencies."""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000990 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
991 help='override deps for the specified (comma-separated) '
992 'platform(s); \'all\' will process all deps_os '
993 'references')
994 parser.add_option('-n', '--nohooks', action='store_true',
995 help='don\'t run hooks after the revert is complete')
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000996 (options, args) = parser.parse_args(args)
997 # --force is implied.
998 options.force = True
maruel@chromium.org2806acc2009-05-15 12:33:34 +0000999 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001000 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00001001 raise gclient_utils.Error('client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001002 return client.RunOnDeps('revert', args)
1003
1004
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001005def CMDrunhooks(parser, args):
1006 """Runs hooks for files that have been modified in the local working copy."""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00001007 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
1008 help='override deps for the specified (comma-separated) '
1009 'platform(s); \'all\' will process all deps_os '
1010 'references')
1011 parser.add_option('-f', '--force', action='store_true', default=True,
1012 help='Deprecated. No effect.')
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001013 (options, args) = parser.parse_args(args)
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001014 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001015 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00001016 raise gclient_utils.Error('client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001017 if options.verbose:
1018 # Print out the .gclient file. This is longer than if we just printed the
1019 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.org116704f2010-06-11 17:34:38 +00001020 print(client.config_content)
maruel@chromium.org5df6a462009-08-28 18:52:26 +00001021 options.force = True
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001022 options.nohooks = False
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001023 return client.RunOnDeps('runhooks', args)
1024
1025
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001026def CMDrevinfo(parser, args):
maruel@chromium.org9eda4112010-06-11 18:56:10 +00001027 """Output revision info mapping for the client and its dependencies.
1028
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00001029 This allows the capture of an overall 'revision' for the source tree that
maruel@chromium.org9eda4112010-06-11 18:56:10 +00001030 can be used to reproduce the same tree in the future. It is only useful for
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00001031 'unpinned dependencies', i.e. DEPS/deps references without a svn revision
1032 number or a git hash. A git branch name isn't 'pinned' since the actual
maruel@chromium.org9eda4112010-06-11 18:56:10 +00001033 commit can change.
1034 """
1035 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
1036 help='override deps for the specified (comma-separated) '
1037 'platform(s); \'all\' will process all deps_os '
1038 'references')
1039 parser.add_option('-s', '--snapshot', action='store_true',
1040 help='creates a snapshot .gclient file of the current '
maruel@chromium.org917aa912010-07-09 20:35:18 +00001041 'version of all repositories to reproduce the tree')
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001042 (options, args) = parser.parse_args(args)
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001043 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001044 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00001045 raise gclient_utils.Error('client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001046 client.PrintRevInfo()
maruel@chromium.org79692d62010-05-14 18:57:13 +00001047 return 0
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001048
1049
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001050def Command(name):
1051 return getattr(sys.modules[__name__], 'CMD' + name, None)
1052
1053
1054def CMDhelp(parser, args):
1055 """Prints list of commands or help for a specific command."""
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001056 (_, args) = parser.parse_args(args)
maruel@chromium.orgddff62d2010-05-17 21:02:36 +00001057 if len(args) == 1:
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001058 return Main(args + ['--help'])
maruel@chromium.orgddff62d2010-05-17 21:02:36 +00001059 parser.print_help()
1060 return 0
1061
1062
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001063def GenUsage(parser, command):
1064 """Modify an OptParse object with the function's documentation."""
1065 obj = Command(command)
1066 if command == 'help':
1067 command = '<command>'
1068 # OptParser.description prefer nicely non-formatted strings.
1069 parser.description = re.sub('[\r\n ]{2,}', ' ', obj.__doc__)
1070 usage = getattr(obj, 'usage', '')
1071 parser.set_usage('%%prog %s [options] %s' % (command, usage))
1072 parser.epilog = getattr(obj, 'epilog', None)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001073
1074
1075def Main(argv):
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001076 """Doesn't parse the arguments here, just find the right subcommand to
1077 execute."""
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001078 try:
1079 # Do it late so all commands are listed.
1080 CMDhelp.usage = ('\n\nCommands are:\n' + '\n'.join([
1081 ' %-10s %s' % (fn[3:], Command(fn[3:]).__doc__.split('\n')[0].strip())
1082 for fn in dir(sys.modules[__name__]) if fn.startswith('CMD')]))
1083 parser = optparse.OptionParser(version='%prog ' + __version__)
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001084 parser.add_option('-v', '--verbose', action='count', default=0,
1085 help='Produces additional output for diagnostics. Can be '
1086 'used up to three times for more logging info.')
1087 parser.add_option('--gclientfile', dest='config_filename',
1088 default=os.environ.get('GCLIENT_FILE', '.gclient'),
1089 help='Specify an alternate %default file')
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001090 # Integrate standard options processing.
1091 old_parser = parser.parse_args
1092 def Parse(args):
1093 (options, args) = old_parser(args)
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001094 level = None
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001095 if options.verbose == 2:
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001096 level = logging.INFO
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001097 elif options.verbose > 2:
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001098 level = logging.DEBUG
1099 logging.basicConfig(level=level,
1100 format='%(module)s(%(lineno)d) %(funcName)s:%(message)s')
1101 options.entries_filename = options.config_filename + '_entries'
maruel@chromium.orge3216c62010-07-08 03:31:43 +00001102
1103 # These hacks need to die.
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001104 if not hasattr(options, 'revisions'):
1105 # GClient.RunOnDeps expects it even if not applicable.
1106 options.revisions = []
1107 if not hasattr(options, 'head'):
1108 options.head = None
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001109 if not hasattr(options, 'nohooks'):
1110 options.nohooks = True
1111 if not hasattr(options, 'deps_os'):
1112 options.deps_os = None
maruel@chromium.orge3216c62010-07-08 03:31:43 +00001113 if not hasattr(options, 'manually_grab_svn_rev'):
1114 options.manually_grab_svn_rev = None
1115 if not hasattr(options, 'force'):
1116 options.force = None
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001117 return (options, args)
1118 parser.parse_args = Parse
1119 # We don't want wordwrapping in epilog (usually examples)
1120 parser.format_epilog = lambda _: parser.epilog or ''
1121 if argv:
1122 command = Command(argv[0])
1123 if command:
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001124 # 'fix' the usage and the description now that we know the subcommand.
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001125 GenUsage(parser, argv[0])
1126 return command(parser, argv[1:])
1127 # Not a known command. Default to help.
1128 GenUsage(parser, 'help')
1129 return CMDhelp(parser, argv)
1130 except gclient_utils.Error, e:
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001131 print >> sys.stderr, 'Error: %s' % str(e)
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001132 return 1
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001133
1134
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001135if '__main__' == __name__:
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001136 sys.exit(Main(sys.argv[1:]))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001137
1138# vim: ts=2:sw=2:tw=80:et: