blob: d62e77ee58a3c8a49d528407e32377c0c5a5ba02 [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.orgbffb9042010-07-22 20:59:36 +0000163 tree = dict((d.name, d) for d in self.tree(False))
164 if self.name in tree:
165 raise gclient_utils.Error(
166 'Dependency %s specified more than once:\n %s\nvs\n %s' %
167 (self.name, tree[self.name].hierarchy(), self.hierarchy()))
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000168 if not isinstance(self.url,
169 (basestring, self.FromImpl, self.FileImpl, None.__class__)):
170 raise gclient_utils.Error('dependency url must be either a string, None, '
171 'File() or From() instead of %s' %
172 self.url.__class__.__name__)
173 if '/' in self.deps_file or '\\' in self.deps_file:
174 raise gclient_utils.Error('deps_file name must not be a path, just a '
175 'filename. %s' % self.deps_file)
176
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000177 def LateOverride(self, url):
178 overriden_url = self.get_custom_deps(self.name, url)
179 if overriden_url != url:
180 self.parsed_url = overriden_url
181 logging.debug('%s, %s was overriden to %s' % (self.name, url,
182 self.parsed_url))
183 elif isinstance(url, self.FromImpl):
184 ref = [dep for dep in self.tree(True) if url.module_name == dep.name]
185 if not len(ref) == 1:
186 raise Exception('Failed to find one reference to %s. %s' % (
187 url.module_name, ref))
188 ref = ref[0]
189 sub_target = url.sub_target_name or url
190 # Make sure the referenced dependency DEPS file is loaded and file the
191 # inner referenced dependency.
192 ref.ParseDepsFile(False)
193 found_dep = None
194 for d in ref.dependencies:
195 if d.name == sub_target:
196 found_dep = d
197 break
198 if not found_dep:
199 raise Exception('Couldn\'t find %s in %s, referenced by %s' % (
200 sub_target, ref.name, self.name))
201 # Call LateOverride() again.
202 self.parsed_url = found_dep.LateOverride(found_dep.url)
203 logging.debug('%s, %s to %s' % (self.name, url, self.parsed_url))
204 elif isinstance(url, basestring):
205 parsed_url = urlparse.urlparse(url)
206 if not parsed_url[0]:
207 # A relative url. Fetch the real base.
208 path = parsed_url[2]
209 if not path.startswith('/'):
210 raise gclient_utils.Error(
211 'relative DEPS entry \'%s\' must begin with a slash' % url)
212 # Create a scm just to query the full url.
213 parent_url = self.parent.parsed_url
214 if isinstance(parent_url, self.FileImpl):
215 parent_url = parent_url.file_location
216 scm = gclient_scm.CreateSCM(parent_url, self.root_dir(), None)
217 self.parsed_url = scm.FullUrlForRelativeUrl(url)
218 else:
219 self.parsed_url = url
220 logging.debug('%s, %s -> %s' % (self.name, url, self.parsed_url))
221 elif isinstance(url, self.FileImpl):
222 self.parsed_url = url
223 logging.debug('%s, %s -> %s (File)' % (self.name, url, self.parsed_url))
224 return self.parsed_url
225
maruel@chromium.org271375b2010-06-23 19:17:38 +0000226 def ParseDepsFile(self, direct_reference):
227 """Parses the DEPS file for this dependency."""
228 if direct_reference:
229 # Maybe it was referenced earlier by a From() keyword but it's now
230 # directly referenced.
231 self.direct_reference = direct_reference
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000232 if self.deps_parsed:
233 return
maruel@chromium.org271375b2010-06-23 19:17:38 +0000234 self.deps_parsed = True
235 filepath = os.path.join(self.root_dir(), self.name, self.deps_file)
236 if not os.path.isfile(filepath):
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000237 return
maruel@chromium.org271375b2010-06-23 19:17:38 +0000238 deps_content = gclient_utils.FileRead(filepath)
maruel@chromium.org0d425922010-06-21 19:22:24 +0000239
maruel@chromium.org271375b2010-06-23 19:17:38 +0000240 # Eval the content.
241 # One thing is unintuitive, vars= {} must happen before Var() use.
242 local_scope = {}
243 var = self.VarImpl(self.custom_vars, local_scope)
244 global_scope = {
245 'File': self.FileImpl,
246 'From': self.FromImpl,
247 'Var': var.Lookup,
248 'deps_os': {},
249 }
maruel@chromium.org5990f9d2010-07-07 18:02:58 +0000250 try:
251 exec(deps_content, global_scope, local_scope)
252 except SyntaxError, e:
253 gclient_utils.SyntaxErrorToError(filepath, e)
maruel@chromium.org271375b2010-06-23 19:17:38 +0000254 deps = local_scope.get('deps', {})
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000255 # load os specific dependencies if defined. these dependencies may
256 # override or extend the values defined by the 'deps' member.
maruel@chromium.org271375b2010-06-23 19:17:38 +0000257 if 'deps_os' in local_scope:
258 for deps_os_key in self.enforced_os():
259 os_deps = local_scope['deps_os'].get(deps_os_key, {})
260 if len(self.enforced_os()) > 1:
261 # Ignore any conflict when including deps for more than one
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000262 # platform, so we collect the broadest set of dependencies available.
263 # We may end up with the wrong revision of something for our
264 # platform, but this is the best we can do.
265 deps.update([x for x in os_deps.items() if not x[0] in deps])
266 else:
267 deps.update(os_deps)
268
maruel@chromium.org271375b2010-06-23 19:17:38 +0000269 self.deps_hooks.extend(local_scope.get('hooks', []))
270
271 # If a line is in custom_deps, but not in the solution, we want to append
272 # this line to the solution.
273 for d in self.custom_deps:
274 if d not in deps:
275 deps[d] = self.custom_deps[d]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000276
277 # If use_relative_paths is set in the DEPS file, regenerate
278 # the dictionary using paths relative to the directory containing
279 # the DEPS file.
maruel@chromium.org271375b2010-06-23 19:17:38 +0000280 use_relative_paths = local_scope.get('use_relative_paths', False)
281 if use_relative_paths:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000282 rel_deps = {}
283 for d, url in deps.items():
284 # normpath is required to allow DEPS to use .. in their
285 # dependency local path.
maruel@chromium.org271375b2010-06-23 19:17:38 +0000286 rel_deps[os.path.normpath(os.path.join(self.name, d))] = url
287 deps = rel_deps
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000288
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000289 # Convert the deps into real Dependency.
290 for name, url in deps.iteritems():
291 if name in [s.name for s in self.dependencies]:
292 raise
293 self.dependencies.append(Dependency(self, name, url))
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000294 logging.info('Loaded: %s' % str(self))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000295
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000296 def RunCommandRecursively(self, options, revision_overrides,
297 command, args, pm):
298 """Runs 'command' before parsing the DEPS in case it's a initial checkout
299 or a revert."""
300 assert self.file_list == []
301 # When running runhooks, there's no need to consult the SCM.
302 # All known hooks are expected to run unconditionally regardless of working
303 # copy state, so skip the SCM status check.
304 run_scm = command not in ('runhooks', None)
305 self.LateOverride(self.url)
306 if run_scm and self.parsed_url:
307 if isinstance(self.parsed_url, self.FileImpl):
308 # Special support for single-file checkout.
309 if not command in (None, 'cleanup', 'diff', 'pack', 'status'):
310 options.revision = self.parsed_url.GetRevision()
311 scm = gclient_scm.SVNWrapper(self.parsed_url.GetPath(),
312 self.root_dir(),
313 self.name)
314 scm.RunCommand('updatesingle', options,
315 args + [self.parsed_url.GetFilename()],
316 self.file_list)
317 else:
318 options.revision = revision_overrides.get(self.name)
319 scm = gclient_scm.CreateSCM(self.parsed_url, self.root_dir(), self.name)
320 scm.RunCommand(command, options, args, self.file_list)
321 self.file_list = [os.path.join(self.name, f.strip())
322 for f in self.file_list]
323 options.revision = None
324 if pm:
325 # The + 1 comes from the fact that .gclient is considered a step in
326 # itself, .i.e. this code is called one time for the .gclient. This is not
327 # conceptually correct but it simplifies code.
328 pm._total = len(self.tree(False)) + 1
329 pm.update()
330 if self.recursion_limit():
331 # Then we can parse the DEPS file.
332 self.ParseDepsFile(True)
333 if pm:
334 pm._total = len(self.tree(False)) + 1
335 pm.update(0)
336 # Parse the dependencies of this dependency.
337 for s in self.dependencies:
338 # TODO(maruel): All these can run concurrently! No need for threads,
339 # just buffer stdout&stderr on pipes and flush as they complete.
340 # Watch out for stdin.
341 s.RunCommandRecursively(options, revision_overrides, command, args, pm)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000342
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000343 def RunHooksRecursively(self, options):
344 """Evaluates all hooks, running actions as needed. RunCommandRecursively()
345 must have been called before to load the DEPS."""
maruel@chromium.orgdc7445d2010-07-09 21:05:29 +0000346 # If "--force" was specified, run all hooks regardless of what files have
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000347 # changed.
348 if self.deps_hooks and self.direct_reference:
349 # TODO(maruel): If the user is using git or git-svn, then we don't know
350 # what files have changed so we always run all hooks. It'd be nice to fix
351 # that.
352 if (options.force or
353 isinstance(self.parsed_url, self.FileImpl) or
354 gclient_scm.GetScmName(self.parsed_url) in ('git', None) or
355 os.path.isdir(os.path.join(self.root_dir(), self.name, '.git'))):
356 for hook_dict in self.deps_hooks:
357 self._RunHookAction(hook_dict, [])
358 else:
359 # TODO(phajdan.jr): We should know exactly when the paths are absolute.
360 # Convert all absolute paths to relative.
361 for i in range(len(self.file_list)):
362 # It depends on the command being executed (like runhooks vs sync).
363 if not os.path.isabs(self.file_list[i]):
364 continue
maruel@chromium.orgdc7445d2010-07-09 21:05:29 +0000365
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000366 prefix = os.path.commonprefix([self.root_dir().lower(),
367 self.file_list[i].lower()])
368 self.file_list[i] = self.file_list[i][len(prefix):]
369
370 # Strip any leading path separators.
371 while (self.file_list[i].startswith('\\') or
372 self.file_list[i].startswith('/')):
373 self.file_list[i] = self.file_list[i][1:]
374
375 # Run hooks on the basis of whether the files from the gclient operation
376 # match each hook's pattern.
377 for hook_dict in self.deps_hooks:
378 pattern = re.compile(hook_dict['pattern'])
379 matching_file_list = [f for f in self.file_list if pattern.search(f)]
380 if matching_file_list:
381 self._RunHookAction(hook_dict, matching_file_list)
382 if self.recursion_limit():
383 for s in self.dependencies:
384 s.RunHooksRecursively(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000385
maruel@chromium.orgeaf61062010-07-07 18:42:39 +0000386 def _RunHookAction(self, hook_dict, matching_file_list):
387 """Runs the action from a single hook."""
388 logging.info(hook_dict)
389 logging.info(matching_file_list)
390 command = hook_dict['action'][:]
391 if command[0] == 'python':
392 # If the hook specified "python" as the first item, the action is a
393 # Python script. Run it by starting a new copy of the same
394 # interpreter.
395 command[0] = sys.executable
396
397 if '$matching_files' in command:
398 splice_index = command.index('$matching_files')
399 command[splice_index:splice_index + 1] = matching_file_list
400
401 # Use a discrete exit status code of 2 to indicate that a hook action
402 # failed. Users of this script may wish to treat hook action failures
403 # differently from VC failures.
404 return gclient_utils.SubprocessCall(command, self.root_dir(), fail_status=2)
405
maruel@chromium.org271375b2010-06-23 19:17:38 +0000406 def root_dir(self):
407 return self.parent.root_dir()
408
409 def enforced_os(self):
410 return self.parent.enforced_os()
411
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000412 def recursion_limit(self):
413 return self.parent.recursion_limit() - 1
414
415 def tree(self, force_all):
416 return self.parent.tree(force_all)
417
418 def get_custom_deps(self, name, url):
419 """Returns a custom deps if applicable."""
420 if self.parent:
421 url = self.parent.get_custom_deps(name, url)
422 # None is a valid return value to disable a dependency.
423 return self.custom_deps.get(name, url)
424
425 def __str__(self):
426 out = []
427 for i in ('name', 'url', 'safesync_url', 'custom_deps', 'custom_vars',
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000428 'deps_hooks', 'file_list'):
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000429 # 'deps_file'
430 if self.__dict__[i]:
431 out.append('%s: %s' % (i, self.__dict__[i]))
432
433 for d in self.dependencies:
434 out.extend([' ' + x for x in str(d).splitlines()])
435 out.append('')
436 return '\n'.join(out)
437
438 def __repr__(self):
439 return '%s: %s' % (self.name, self.url)
440
maruel@chromium.orgbffb9042010-07-22 20:59:36 +0000441 def hierarchy(self):
442 "Returns a human-readable hierarchical reference to a Dependency."
443 out = '%s(%s)' % (self.name, self.url)
444 i = self.parent
445 while i and i.name:
446 out = '%s(%s) -> %s' % (i.name, i.url, out)
447 i = i.parent
448 return out
449
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000450
451class GClient(Dependency):
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000452 """Object that represent a gclient checkout. A tree of Dependency(), one per
453 solution or DEPS entry."""
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000454
455 DEPS_OS_CHOICES = {
456 "win32": "win",
457 "win": "win",
458 "cygwin": "win",
459 "darwin": "mac",
460 "mac": "mac",
461 "unix": "unix",
462 "linux": "unix",
463 "linux2": "unix",
464 }
465
466 DEFAULT_CLIENT_FILE_TEXT = ("""\
467solutions = [
468 { "name" : "%(solution_name)s",
469 "url" : "%(solution_url)s",
470 "custom_deps" : {
471 },
maruel@chromium.org73e21142010-07-05 13:32:01 +0000472 "safesync_url": "%(safesync_url)s",
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000473 },
474]
475""")
476
477 DEFAULT_SNAPSHOT_SOLUTION_TEXT = ("""\
478 { "name" : "%(solution_name)s",
479 "url" : "%(solution_url)s",
480 "custom_deps" : {
maruel@chromium.org73e21142010-07-05 13:32:01 +0000481%(solution_deps)s },
482 "safesync_url": "%(safesync_url)s",
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000483 },
484""")
485
486 DEFAULT_SNAPSHOT_FILE_TEXT = ("""\
487# Snapshot generated with gclient revinfo --snapshot
488solutions = [
maruel@chromium.org73e21142010-07-05 13:32:01 +0000489%(solution_list)s]
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000490""")
491
492 def __init__(self, root_dir, options):
493 Dependency.__init__(self, None, None, None)
maruel@chromium.org0d425922010-06-21 19:22:24 +0000494 self._options = options
maruel@chromium.org271375b2010-06-23 19:17:38 +0000495 if options.deps_os:
496 enforced_os = options.deps_os.split(',')
497 else:
498 enforced_os = [self.DEPS_OS_CHOICES.get(sys.platform, 'unix')]
499 if 'all' in enforced_os:
500 enforced_os = self.DEPS_OS_CHOICES.itervalues()
501 self._enforced_os = list(set(enforced_os))
502 self._root_dir = root_dir
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000503 self.config_content = None
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000504 # Do not change previous behavior. Only solution level and immediate DEPS
505 # are processed.
506 self._recursion_limit = 2
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000507
508 def SetConfig(self, content):
509 assert self.dependencies == []
510 config_dict = {}
511 self.config_content = content
512 try:
513 exec(content, config_dict)
514 except SyntaxError, e:
maruel@chromium.org5990f9d2010-07-07 18:02:58 +0000515 gclient_utils.SyntaxErrorToError('.gclient', e)
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000516 for s in config_dict.get('solutions', []):
maruel@chromium.org81843b82010-06-28 16:49:26 +0000517 try:
518 self.dependencies.append(Dependency(
519 self, s['name'], s['url'],
520 s.get('safesync_url', None),
521 s.get('custom_deps', {}),
522 s.get('custom_vars', {})))
523 except KeyError:
524 raise gclient_utils.Error('Invalid .gclient file. Solution is '
525 'incomplete: %s' % s)
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000526 # .gclient can have hooks.
527 self.deps_hooks = config_dict.get('hooks', [])
528
529 def SaveConfig(self):
530 gclient_utils.FileWrite(os.path.join(self.root_dir(),
531 self._options.config_filename),
532 self.config_content)
533
534 @staticmethod
535 def LoadCurrentConfig(options):
536 """Searches for and loads a .gclient file relative to the current working
537 dir. Returns a GClient object."""
538 path = gclient_utils.FindGclientRoot(os.getcwd(), options.config_filename)
539 if not path:
540 return None
541 client = GClient(path, options)
542 client.SetConfig(gclient_utils.FileRead(
543 os.path.join(path, options.config_filename)))
544 return client
545
546 def SetDefaultConfig(self, solution_name, solution_url, safesync_url):
547 self.SetConfig(self.DEFAULT_CLIENT_FILE_TEXT % {
548 'solution_name': solution_name,
549 'solution_url': solution_url,
550 'safesync_url' : safesync_url,
551 })
552
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000553 def _SaveEntries(self):
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000554 """Creates a .gclient_entries file to record the list of unique checkouts.
555
556 The .gclient_entries file lives in the same directory as .gclient.
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000557 """
558 # Sometimes pprint.pformat will use {', sometimes it'll use { ' ... It
559 # makes testing a bit too fun.
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000560 result = 'entries = {\n'
561 for entry in self.tree(False):
562 # Skip over File() dependencies as we can't version them.
563 if not isinstance(entry.parsed_url, self.FileImpl):
564 result += ' %s: %s,\n' % (pprint.pformat(entry.name),
565 pprint.pformat(entry.parsed_url))
566 result += '}\n'
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000567 file_path = os.path.join(self.root_dir(), self._options.entries_filename)
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000568 logging.info(result)
569 gclient_utils.FileWrite(file_path, result)
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000570
571 def _ReadEntries(self):
572 """Read the .gclient_entries file for the given client.
573
574 Returns:
575 A sequence of solution names, which will be empty if there is the
576 entries file hasn't been created yet.
577 """
578 scope = {}
579 filename = os.path.join(self.root_dir(), self._options.entries_filename)
580 if not os.path.exists(filename):
maruel@chromium.org73e21142010-07-05 13:32:01 +0000581 return {}
maruel@chromium.org5990f9d2010-07-07 18:02:58 +0000582 try:
583 exec(gclient_utils.FileRead(filename), scope)
584 except SyntaxError, e:
585 gclient_utils.SyntaxErrorToError(filename, e)
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000586 return scope['entries']
587
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000588 def _EnforceRevisions(self):
maruel@chromium.org918a9ae2010-05-28 15:50:30 +0000589 """Checks for revision overrides."""
590 revision_overrides = {}
maruel@chromium.org307d1792010-05-31 20:03:13 +0000591 if self._options.head:
592 return revision_overrides
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000593 for s in self.dependencies:
594 if not s.safesync_url:
maruel@chromium.org307d1792010-05-31 20:03:13 +0000595 continue
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000596 handle = urllib.urlopen(s.safesync_url)
maruel@chromium.org307d1792010-05-31 20:03:13 +0000597 rev = handle.read().strip()
598 handle.close()
599 if len(rev):
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000600 self._options.revisions.append('%s@%s' % (s.name, rev))
maruel@chromium.org307d1792010-05-31 20:03:13 +0000601 if not self._options.revisions:
602 return revision_overrides
603 # --revision will take over safesync_url.
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000604 solutions_names = [s.name for s in self.dependencies]
maruel@chromium.org307d1792010-05-31 20:03:13 +0000605 index = 0
606 for revision in self._options.revisions:
607 if not '@' in revision:
608 # Support for --revision 123
609 revision = '%s@%s' % (solutions_names[index], revision)
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000610 sol, rev = revision.split('@', 1)
maruel@chromium.org307d1792010-05-31 20:03:13 +0000611 if not sol in solutions_names:
612 #raise gclient_utils.Error('%s is not a valid solution.' % sol)
613 print >> sys.stderr, ('Please fix your script, having invalid '
614 '--revision flags will soon considered an error.')
615 else:
maruel@chromium.org918a9ae2010-05-28 15:50:30 +0000616 revision_overrides[sol] = rev
maruel@chromium.org307d1792010-05-31 20:03:13 +0000617 index += 1
maruel@chromium.org918a9ae2010-05-28 15:50:30 +0000618 return revision_overrides
619
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000620 def RunOnDeps(self, command, args):
621 """Runs a command on each dependency in a client and its dependencies.
622
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000623 Args:
624 command: The command to use (e.g., 'status' or 'diff')
625 args: list of str - extra arguments to add to the command line.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000626 """
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000627 if not self.dependencies:
maruel@chromium.org73e21142010-07-05 13:32:01 +0000628 raise gclient_utils.Error('No solution specified')
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000629 revision_overrides = self._EnforceRevisions()
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000630 pm = None
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000631 if command == 'update' and not self._options.verbose:
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000632 pm = Progress('Syncing projects', len(self.tree(False)) + 1)
633 self.RunCommandRecursively(self._options, revision_overrides,
634 command, args, pm)
635 if pm:
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000636 pm.end()
piman@chromium.org6f363722010-04-27 00:41:09 +0000637
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000638 # Once all the dependencies have been processed, it's now safe to run the
639 # hooks.
640 if not self._options.nohooks:
641 self.RunHooksRecursively(self._options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000642
643 if command == 'update':
ajwong@chromium.orgcdcee802009-06-23 15:30:42 +0000644 # Notify the user if there is an orphaned entry in their working copy.
645 # Only delete the directory if there are no changes in it, and
646 # delete_unversioned_trees is set to true.
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000647 entries = [i.name for i in self.tree(False)]
648 for entry, prev_url in self._ReadEntries().iteritems():
maruel@chromium.orgc5e9aec2009-08-03 18:25:56 +0000649 # Fix path separator on Windows.
650 entry_fixed = entry.replace('/', os.path.sep)
maruel@chromium.org75a59272010-06-11 22:34:03 +0000651 e_dir = os.path.join(self.root_dir(), entry_fixed)
maruel@chromium.orgc5e9aec2009-08-03 18:25:56 +0000652 # Use entry and not entry_fixed there.
maruel@chromium.org0329e672009-05-13 18:41:04 +0000653 if entry not in entries and os.path.exists(e_dir):
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000654 file_list = []
655 scm = gclient_scm.CreateSCM(prev_url, self.root_dir(), entry_fixed)
656 scm.status(self._options, [], file_list)
657 modified_files = file_list != []
msb@chromium.org83017012009-09-28 18:52:12 +0000658 if not self._options.delete_unversioned_trees or modified_files:
maruel@chromium.orgc5e9aec2009-08-03 18:25:56 +0000659 # There are modified files in this entry. Keep warning until
660 # removed.
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000661 print(('\nWARNING: \'%s\' is no longer part of this client. '
662 'It is recommended that you manually remove it.\n') %
maruel@chromium.orgc5e9aec2009-08-03 18:25:56 +0000663 entry_fixed)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000664 else:
665 # Delete the entry
maruel@chromium.org73e21142010-07-05 13:32:01 +0000666 print('\n________ deleting \'%s\' in \'%s\'' % (
667 entry_fixed, self.root_dir()))
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000668 gclient_utils.RemoveDirectory(e_dir)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000669 # record the current list of entries for next time
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000670 self._SaveEntries()
maruel@chromium.org17cdf762010-05-28 17:30:52 +0000671 return 0
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000672
673 def PrintRevInfo(self):
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000674 """Output revision info mapping for the client and its dependencies.
675
676 This allows the capture of an overall "revision" for the source tree that
maruel@chromium.org918a9ae2010-05-28 15:50:30 +0000677 can be used to reproduce the same tree in the future. It is only useful for
678 "unpinned dependencies", i.e. DEPS/deps references without a svn revision
679 number or a git hash. A git branch name isn't "pinned" since the actual
680 commit can change.
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000681
682 The --snapshot option allows creating a .gclient file to reproduce the tree.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000683 """
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000684 if not self.dependencies:
maruel@chromium.org73e21142010-07-05 13:32:01 +0000685 raise gclient_utils.Error('No solution specified')
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000686 # Load all the settings.
687 self.RunCommandRecursively(self._options, {}, None, [], None)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000688
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000689 def GetURLAndRev(name, original_url):
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000690 """Returns the revision-qualified SCM url."""
691 if original_url is None:
maruel@chromium.orgbaa578e2010-07-12 17:36:59 +0000692 return None
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000693 if isinstance(original_url, self.FileImpl):
694 original_url = original_url.file_location
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000695 url, _ = gclient_utils.SplitUrlRevision(original_url)
maruel@chromium.org75a59272010-06-11 22:34:03 +0000696 scm = gclient_scm.CreateSCM(original_url, self.root_dir(), name)
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000697 if not os.path.isdir(scm.checkout_path):
698 return None
maruel@chromium.orgbaa578e2010-07-12 17:36:59 +0000699 return '%s@%s' % (url, scm.revinfo(self._options, [], None))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000700
maruel@chromium.orgbaa578e2010-07-12 17:36:59 +0000701 if self._options.snapshot:
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000702 new_gclient = ''
703 # First level at .gclient
704 for d in self.dependencies:
705 entries = {}
706 def GrabDeps(sol):
707 """Recursively grab dependencies."""
708 for i in sol.dependencies:
709 entries[i.name] = GetURLAndRev(i.name, i.parsed_url)
710 GrabDeps(i)
711 GrabDeps(d)
712 custom_deps = []
713 for k in sorted(entries.keys()):
714 if entries[k]:
715 # Quotes aren't escaped...
716 custom_deps.append(' \"%s\": \'%s\',\n' % (k, entries[k]))
717 else:
718 custom_deps.append(' \"%s\": None,\n' % k)
719 new_gclient += self.DEFAULT_SNAPSHOT_SOLUTION_TEXT % {
720 'solution_name': d.name,
721 'solution_url': d.url,
722 'safesync_url' : d.safesync_url or '',
723 'solution_deps': ''.join(custom_deps),
724 }
725 # Print the snapshot configuration file
726 print(self.DEFAULT_SNAPSHOT_FILE_TEXT % {'solution_list': new_gclient})
nasser@codeaurora.orgde8f3522010-03-11 23:47:44 +0000727 else:
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000728 entries = sorted(self.tree(False), key=lambda i: i.name)
729 for entry in entries:
730 entry_url = GetURLAndRev(entry.name, entry.parsed_url)
731 line = '%s: %s' % (entry.name, entry_url)
732 if not entry is entries[-1]:
733 line += ';'
734 print line
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000735
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000736 def ParseDepsFile(self, direct_reference):
737 """No DEPS to parse for a .gclient file."""
738 self.direct_reference = direct_reference
739 self.deps_parsed = True
740
maruel@chromium.org75a59272010-06-11 22:34:03 +0000741 def root_dir(self):
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000742 """Root directory of gclient checkout."""
maruel@chromium.org75a59272010-06-11 22:34:03 +0000743 return self._root_dir
744
maruel@chromium.org271375b2010-06-23 19:17:38 +0000745 def enforced_os(self):
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000746 """What deps_os entries that are to be parsed."""
maruel@chromium.org271375b2010-06-23 19:17:38 +0000747 return self._enforced_os
748
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000749 def recursion_limit(self):
750 """How recursive can each dependencies in DEPS file can load DEPS file."""
751 return self._recursion_limit
752
753 def tree(self, force_all):
754 """Returns a flat list of all the dependencies."""
755 def subtree(dep):
756 if not force_all and not dep.direct_reference:
757 # Was loaded from a From() keyword in a DEPS file, don't load all its
758 # dependencies.
759 return []
760 result = dep.dependencies[:]
761 for d in dep.dependencies:
762 result.extend(subtree(d))
763 return result
764 return subtree(self)
765
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000766
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000767#### gclient commands.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000768
769
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000770def CMDcleanup(parser, args):
771 """Cleans up all working copies.
maruel@chromium.orgddff62d2010-05-17 21:02:36 +0000772
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000773Mostly svn-specific. Simply runs 'svn cleanup' for each module.
maruel@chromium.org79692d62010-05-14 18:57:13 +0000774"""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000775 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
776 help='override deps for the specified (comma-separated) '
777 'platform(s); \'all\' will process all deps_os '
778 'references')
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000779 (options, args) = parser.parse_args(args)
maruel@chromium.org2806acc2009-05-15 12:33:34 +0000780 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000781 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000782 raise gclient_utils.Error('client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000783 if options.verbose:
784 # Print out the .gclient file. This is longer than if we just printed the
785 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.org116704f2010-06-11 17:34:38 +0000786 print(client.config_content)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000787 return client.RunOnDeps('cleanup', args)
788
789
piman@chromium.org4b90e3a2010-07-01 20:28:26 +0000790@attr('usage', '[command] [args ...]')
791def CMDrecurse(parser, args):
792 """Operates on all the entries.
793
794 Runs a shell command on all entries.
795 """
796 # Stop parsing at the first non-arg so that these go through to the command
797 parser.disable_interspersed_args()
798 parser.add_option('-s', '--scm', action='append', default=[],
799 help='choose scm types to operate upon')
800 options, args = parser.parse_args(args)
801 root, entries = gclient_utils.GetGClientRootAndEntries()
802 scm_set = set()
803 for scm in options.scm:
804 scm_set.update(scm.split(','))
805
806 # Pass in the SCM type as an env variable
807 env = os.environ.copy()
808
809 for path, url in entries.iteritems():
810 scm = gclient_scm.GetScmName(url)
811 if scm_set and scm not in scm_set:
812 continue
813 dir = os.path.normpath(os.path.join(root, path))
814 env['GCLIENT_SCM'] = scm
815 env['GCLIENT_URL'] = url
816 subprocess.Popen(args, cwd=dir, env=env).communicate()
817
818
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000819@attr('usage', '[url] [safesync url]')
820def CMDconfig(parser, args):
maruel@chromium.orgddff62d2010-05-17 21:02:36 +0000821 """Create a .gclient file in the current directory.
822
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000823This specifies the configuration for further commands. After update/sync,
maruel@chromium.org79692d62010-05-14 18:57:13 +0000824top-level DEPS files in each module are read to determine dependent
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000825modules to operate on as well. If optional [url] parameter is
maruel@chromium.org79692d62010-05-14 18:57:13 +0000826provided, then configuration is read from a specified Subversion server
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000827URL.
maruel@chromium.org79692d62010-05-14 18:57:13 +0000828"""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000829 parser.add_option('--spec',
830 help='create a gclient file containing the provided '
831 'string. Due to Cygwin/Python brokenness, it '
832 'probably can\'t contain any newlines.')
833 parser.add_option('--name',
834 help='overrides the default name for the solution')
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000835 (options, args) = parser.parse_args(args)
maruel@chromium.org5fc2a332010-05-26 19:37:15 +0000836 if ((options.spec and args) or len(args) > 2 or
837 (not options.spec and not args)):
838 parser.error('Inconsistent arguments. Use either --spec or one or 2 args')
839
maruel@chromium.org0329e672009-05-13 18:41:04 +0000840 if os.path.exists(options.config_filename):
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000841 raise gclient_utils.Error('%s file already exists in the current directory'
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000842 % options.config_filename)
maruel@chromium.org2806acc2009-05-15 12:33:34 +0000843 client = GClient('.', options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000844 if options.spec:
845 client.SetConfig(options.spec)
846 else:
maruel@chromium.org1ab7ffc2009-06-03 17:21:37 +0000847 base_url = args[0].rstrip('/')
iposva@chromium.org8cf7a392010-04-07 17:20:26 +0000848 if not options.name:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000849 name = base_url.split('/')[-1]
iposva@chromium.org8cf7a392010-04-07 17:20:26 +0000850 else:
851 # specify an alternate relpath for the given URL.
852 name = options.name
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000853 safesync_url = ''
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000854 if len(args) > 1:
855 safesync_url = args[1]
856 client.SetDefaultConfig(name, base_url, safesync_url)
857 client.SaveConfig()
maruel@chromium.org79692d62010-05-14 18:57:13 +0000858 return 0
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000859
860
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000861def CMDexport(parser, args):
maruel@chromium.orgddff62d2010-05-17 21:02:36 +0000862 """Wrapper for svn export for all managed directories."""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000863 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
864 help='override deps for the specified (comma-separated) '
865 'platform(s); \'all\' will process all deps_os '
866 'references')
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000867 (options, args) = parser.parse_args(args)
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +0000868 if len(args) != 1:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000869 raise gclient_utils.Error('Need directory name')
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +0000870 client = GClient.LoadCurrentConfig(options)
871
872 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000873 raise gclient_utils.Error('client not configured; see \'gclient config\'')
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +0000874
875 if options.verbose:
876 # Print out the .gclient file. This is longer than if we just printed the
877 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.org116704f2010-06-11 17:34:38 +0000878 print(client.config_content)
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +0000879 return client.RunOnDeps('export', args)
880
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000881
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000882@attr('epilog', """Example:
883 gclient pack > patch.txt
884 generate simple patch for configured client and dependences
885""")
886def CMDpack(parser, args):
maruel@chromium.org79692d62010-05-14 18:57:13 +0000887 """Generate a patch which can be applied at the root of the tree.
maruel@chromium.orgddff62d2010-05-17 21:02:36 +0000888
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000889Internally, runs 'svn diff'/'git diff' on each checked out module and
maruel@chromium.org79692d62010-05-14 18:57:13 +0000890dependencies, and performs minimal postprocessing of the output. The
891resulting patch is printed to stdout and can be applied to a freshly
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000892checked out tree via 'patch -p0 < patchfile'.
maruel@chromium.org79692d62010-05-14 18:57:13 +0000893"""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000894 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
895 help='override deps for the specified (comma-separated) '
896 'platform(s); \'all\' will process all deps_os '
897 'references')
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000898 (options, args) = parser.parse_args(args)
kbr@google.comab318592009-09-04 00:54:55 +0000899 client = GClient.LoadCurrentConfig(options)
900 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000901 raise gclient_utils.Error('client not configured; see \'gclient config\'')
kbr@google.comab318592009-09-04 00:54:55 +0000902 if options.verbose:
903 # Print out the .gclient file. This is longer than if we just printed the
904 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.org116704f2010-06-11 17:34:38 +0000905 print(client.config_content)
kbr@google.comab318592009-09-04 00:54:55 +0000906 return client.RunOnDeps('pack', args)
907
908
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000909def CMDstatus(parser, args):
910 """Show modification status for every dependencies."""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000911 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
912 help='override deps for the specified (comma-separated) '
913 'platform(s); \'all\' will process all deps_os '
914 'references')
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000915 (options, args) = parser.parse_args(args)
maruel@chromium.org2806acc2009-05-15 12:33:34 +0000916 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000917 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000918 raise gclient_utils.Error('client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000919 if options.verbose:
920 # Print out the .gclient file. This is longer than if we just printed the
921 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.org116704f2010-06-11 17:34:38 +0000922 print(client.config_content)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000923 return client.RunOnDeps('status', args)
924
925
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000926@attr('epilog', """Examples:
maruel@chromium.org79692d62010-05-14 18:57:13 +0000927 gclient sync
928 update files from SCM according to current configuration,
929 *for modules which have changed since last update or sync*
930 gclient sync --force
931 update files from SCM according to current configuration, for
932 all modules (useful for recovering files deleted from local copy)
933 gclient sync --revision src@31000
934 update src directory to r31000
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000935""")
936def CMDsync(parser, args):
937 """Checkout/update all modules."""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000938 parser.add_option('-f', '--force', action='store_true',
939 help='force update even for unchanged modules')
940 parser.add_option('-n', '--nohooks', action='store_true',
941 help='don\'t run hooks after the update is complete')
942 parser.add_option('-r', '--revision', action='append',
943 dest='revisions', metavar='REV', default=[],
944 help='Enforces revision/hash for the solutions with the '
945 'format src@rev. The src@ part is optional and can be '
946 'skipped. -r can be used multiple times when .gclient '
947 'has multiple solutions configured and will work even '
948 'if the src@ part is skipped.')
949 parser.add_option('-H', '--head', action='store_true',
950 help='skips any safesync_urls specified in '
951 'configured solutions and sync to head instead')
952 parser.add_option('-D', '--delete_unversioned_trees', action='store_true',
953 help='delete any unexpected unversioned trees '
954 'that are in the checkout')
955 parser.add_option('-R', '--reset', action='store_true',
956 help='resets any local changes before updating (git only)')
957 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
958 help='override deps for the specified (comma-separated) '
959 'platform(s); \'all\' will process all deps_os '
960 'references')
961 parser.add_option('-m', '--manually_grab_svn_rev', action='store_true',
962 help='Skip svn up whenever possible by requesting '
963 'actual HEAD revision from the repository')
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000964 (options, args) = parser.parse_args(args)
maruel@chromium.org2806acc2009-05-15 12:33:34 +0000965 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000966
967 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000968 raise gclient_utils.Error('client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000969
maruel@chromium.org307d1792010-05-31 20:03:13 +0000970 if options.revisions and options.head:
971 # TODO(maruel): Make it a parser.error if it doesn't break any builder.
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000972 print('Warning: you cannot use both --head and --revision')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000973
974 if options.verbose:
975 # Print out the .gclient file. This is longer than if we just printed the
976 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.org116704f2010-06-11 17:34:38 +0000977 print(client.config_content)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000978 return client.RunOnDeps('update', args)
979
980
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000981def CMDupdate(parser, args):
maruel@chromium.orgddff62d2010-05-17 21:02:36 +0000982 """Alias for the sync command. Deprecated."""
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000983 return CMDsync(parser, args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000984
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000985def CMDdiff(parser, args):
986 """Displays local diff for every dependencies."""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000987 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
988 help='override deps for the specified (comma-separated) '
989 'platform(s); \'all\' will process all deps_os '
990 'references')
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000991 (options, args) = parser.parse_args(args)
maruel@chromium.org2806acc2009-05-15 12:33:34 +0000992 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000993 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000994 raise gclient_utils.Error('client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000995 if options.verbose:
996 # Print out the .gclient file. This is longer than if we just printed the
997 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.org116704f2010-06-11 17:34:38 +0000998 print(client.config_content)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000999 return client.RunOnDeps('diff', args)
1000
1001
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001002def CMDrevert(parser, args):
1003 """Revert all modifications in every dependencies."""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00001004 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
1005 help='override deps for the specified (comma-separated) '
1006 'platform(s); \'all\' will process all deps_os '
1007 'references')
1008 parser.add_option('-n', '--nohooks', action='store_true',
1009 help='don\'t run hooks after the revert is complete')
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001010 (options, args) = parser.parse_args(args)
1011 # --force is implied.
1012 options.force = True
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001013 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001014 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00001015 raise gclient_utils.Error('client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001016 return client.RunOnDeps('revert', args)
1017
1018
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001019def CMDrunhooks(parser, args):
1020 """Runs hooks for files that have been modified in the local working copy."""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00001021 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
1022 help='override deps for the specified (comma-separated) '
1023 'platform(s); \'all\' will process all deps_os '
1024 'references')
1025 parser.add_option('-f', '--force', action='store_true', default=True,
1026 help='Deprecated. No effect.')
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001027 (options, args) = parser.parse_args(args)
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001028 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001029 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00001030 raise gclient_utils.Error('client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001031 if options.verbose:
1032 # Print out the .gclient file. This is longer than if we just printed the
1033 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.org116704f2010-06-11 17:34:38 +00001034 print(client.config_content)
maruel@chromium.org5df6a462009-08-28 18:52:26 +00001035 options.force = True
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001036 options.nohooks = False
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001037 return client.RunOnDeps('runhooks', args)
1038
1039
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001040def CMDrevinfo(parser, args):
maruel@chromium.org9eda4112010-06-11 18:56:10 +00001041 """Output revision info mapping for the client and its dependencies.
1042
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00001043 This allows the capture of an overall 'revision' for the source tree that
maruel@chromium.org9eda4112010-06-11 18:56:10 +00001044 can be used to reproduce the same tree in the future. It is only useful for
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00001045 'unpinned dependencies', i.e. DEPS/deps references without a svn revision
1046 number or a git hash. A git branch name isn't 'pinned' since the actual
maruel@chromium.org9eda4112010-06-11 18:56:10 +00001047 commit can change.
1048 """
1049 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
1050 help='override deps for the specified (comma-separated) '
1051 'platform(s); \'all\' will process all deps_os '
1052 'references')
1053 parser.add_option('-s', '--snapshot', action='store_true',
1054 help='creates a snapshot .gclient file of the current '
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +00001055 'version of all repositories to reproduce the tree')
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001056 (options, args) = parser.parse_args(args)
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001057 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001058 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00001059 raise gclient_utils.Error('client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001060 client.PrintRevInfo()
maruel@chromium.org79692d62010-05-14 18:57:13 +00001061 return 0
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001062
1063
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001064def Command(name):
1065 return getattr(sys.modules[__name__], 'CMD' + name, None)
1066
1067
1068def CMDhelp(parser, args):
1069 """Prints list of commands or help for a specific command."""
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001070 (_, args) = parser.parse_args(args)
maruel@chromium.orgddff62d2010-05-17 21:02:36 +00001071 if len(args) == 1:
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001072 return Main(args + ['--help'])
maruel@chromium.orgddff62d2010-05-17 21:02:36 +00001073 parser.print_help()
1074 return 0
1075
1076
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001077def GenUsage(parser, command):
1078 """Modify an OptParse object with the function's documentation."""
1079 obj = Command(command)
1080 if command == 'help':
1081 command = '<command>'
1082 # OptParser.description prefer nicely non-formatted strings.
1083 parser.description = re.sub('[\r\n ]{2,}', ' ', obj.__doc__)
1084 usage = getattr(obj, 'usage', '')
1085 parser.set_usage('%%prog %s [options] %s' % (command, usage))
1086 parser.epilog = getattr(obj, 'epilog', None)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001087
1088
1089def Main(argv):
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001090 """Doesn't parse the arguments here, just find the right subcommand to
1091 execute."""
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001092 try:
1093 # Do it late so all commands are listed.
1094 CMDhelp.usage = ('\n\nCommands are:\n' + '\n'.join([
1095 ' %-10s %s' % (fn[3:], Command(fn[3:]).__doc__.split('\n')[0].strip())
1096 for fn in dir(sys.modules[__name__]) if fn.startswith('CMD')]))
1097 parser = optparse.OptionParser(version='%prog ' + __version__)
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001098 parser.add_option('-v', '--verbose', action='count', default=0,
1099 help='Produces additional output for diagnostics. Can be '
1100 'used up to three times for more logging info.')
1101 parser.add_option('--gclientfile', dest='config_filename',
1102 default=os.environ.get('GCLIENT_FILE', '.gclient'),
1103 help='Specify an alternate %default file')
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001104 # Integrate standard options processing.
1105 old_parser = parser.parse_args
1106 def Parse(args):
1107 (options, args) = old_parser(args)
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001108 level = None
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001109 if options.verbose == 2:
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001110 level = logging.INFO
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001111 elif options.verbose > 2:
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001112 level = logging.DEBUG
1113 logging.basicConfig(level=level,
1114 format='%(module)s(%(lineno)d) %(funcName)s:%(message)s')
1115 options.entries_filename = options.config_filename + '_entries'
maruel@chromium.orge3216c62010-07-08 03:31:43 +00001116
1117 # These hacks need to die.
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001118 if not hasattr(options, 'revisions'):
1119 # GClient.RunOnDeps expects it even if not applicable.
1120 options.revisions = []
1121 if not hasattr(options, 'head'):
1122 options.head = None
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001123 if not hasattr(options, 'nohooks'):
1124 options.nohooks = True
1125 if not hasattr(options, 'deps_os'):
1126 options.deps_os = None
maruel@chromium.orge3216c62010-07-08 03:31:43 +00001127 if not hasattr(options, 'manually_grab_svn_rev'):
1128 options.manually_grab_svn_rev = None
1129 if not hasattr(options, 'force'):
1130 options.force = None
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001131 return (options, args)
1132 parser.parse_args = Parse
1133 # We don't want wordwrapping in epilog (usually examples)
1134 parser.format_epilog = lambda _: parser.epilog or ''
1135 if argv:
1136 command = Command(argv[0])
1137 if command:
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001138 # 'fix' the usage and the description now that we know the subcommand.
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001139 GenUsage(parser, argv[0])
1140 return command(parser, argv[1:])
1141 # Not a known command. Default to help.
1142 GenUsage(parser, 'help')
1143 return CMDhelp(parser, argv)
1144 except gclient_utils.Error, e:
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001145 print >> sys.stderr, 'Error: %s' % str(e)
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001146 return 1
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001147
1148
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001149if '__main__' == __name__:
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001150 sys.exit(Main(sys.argv[1:]))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001151
1152# vim: ts=2:sw=2:tw=80:et: