blob: 791c9352dfeec6f0c25f35ac98d5513fd250c765 [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.org85c2a192010-07-22 21:14:43 +0000157 # If it is not set to True, the dependency wasn't processed for its child
158 # dependency, i.e. its DEPS wasn't read.
maruel@chromium.org271375b2010-06-23 19:17:38 +0000159 self.deps_parsed = False
maruel@chromium.org85c2a192010-07-22 21:14:43 +0000160 # A direct reference is dependency that is referenced by a deps, deps_os or
161 # solution. A indirect one is one that was loaded with From() or that
162 # exceeded recursion limit.
maruel@chromium.org271375b2010-06-23 19:17:38 +0000163 self.direct_reference = False
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000164
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000165 # Sanity checks
166 if not self.name and self.parent:
167 raise gclient_utils.Error('Dependency without name')
maruel@chromium.orgbffb9042010-07-22 20:59:36 +0000168 tree = dict((d.name, d) for d in self.tree(False))
169 if self.name in tree:
170 raise gclient_utils.Error(
171 'Dependency %s specified more than once:\n %s\nvs\n %s' %
172 (self.name, tree[self.name].hierarchy(), self.hierarchy()))
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000173 if not isinstance(self.url,
174 (basestring, self.FromImpl, self.FileImpl, None.__class__)):
175 raise gclient_utils.Error('dependency url must be either a string, None, '
176 'File() or From() instead of %s' %
177 self.url.__class__.__name__)
178 if '/' in self.deps_file or '\\' in self.deps_file:
179 raise gclient_utils.Error('deps_file name must not be a path, just a '
180 'filename. %s' % self.deps_file)
181
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000182 def LateOverride(self, url):
183 overriden_url = self.get_custom_deps(self.name, url)
184 if overriden_url != url:
185 self.parsed_url = overriden_url
186 logging.debug('%s, %s was overriden to %s' % (self.name, url,
187 self.parsed_url))
188 elif isinstance(url, self.FromImpl):
189 ref = [dep for dep in self.tree(True) if url.module_name == dep.name]
190 if not len(ref) == 1:
191 raise Exception('Failed to find one reference to %s. %s' % (
192 url.module_name, ref))
193 ref = ref[0]
194 sub_target = url.sub_target_name or url
195 # Make sure the referenced dependency DEPS file is loaded and file the
196 # inner referenced dependency.
197 ref.ParseDepsFile(False)
198 found_dep = None
199 for d in ref.dependencies:
200 if d.name == sub_target:
201 found_dep = d
202 break
203 if not found_dep:
204 raise Exception('Couldn\'t find %s in %s, referenced by %s' % (
205 sub_target, ref.name, self.name))
206 # Call LateOverride() again.
207 self.parsed_url = found_dep.LateOverride(found_dep.url)
208 logging.debug('%s, %s to %s' % (self.name, url, self.parsed_url))
209 elif isinstance(url, basestring):
210 parsed_url = urlparse.urlparse(url)
211 if not parsed_url[0]:
212 # A relative url. Fetch the real base.
213 path = parsed_url[2]
214 if not path.startswith('/'):
215 raise gclient_utils.Error(
216 'relative DEPS entry \'%s\' must begin with a slash' % url)
217 # Create a scm just to query the full url.
218 parent_url = self.parent.parsed_url
219 if isinstance(parent_url, self.FileImpl):
220 parent_url = parent_url.file_location
221 scm = gclient_scm.CreateSCM(parent_url, self.root_dir(), None)
222 self.parsed_url = scm.FullUrlForRelativeUrl(url)
223 else:
224 self.parsed_url = url
225 logging.debug('%s, %s -> %s' % (self.name, url, self.parsed_url))
226 elif isinstance(url, self.FileImpl):
227 self.parsed_url = url
228 logging.debug('%s, %s -> %s (File)' % (self.name, url, self.parsed_url))
229 return self.parsed_url
230
maruel@chromium.org271375b2010-06-23 19:17:38 +0000231 def ParseDepsFile(self, direct_reference):
232 """Parses the DEPS file for this dependency."""
233 if direct_reference:
234 # Maybe it was referenced earlier by a From() keyword but it's now
235 # directly referenced.
236 self.direct_reference = direct_reference
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000237 if self.deps_parsed:
238 return
maruel@chromium.org271375b2010-06-23 19:17:38 +0000239 self.deps_parsed = True
240 filepath = os.path.join(self.root_dir(), self.name, self.deps_file)
241 if not os.path.isfile(filepath):
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000242 return
maruel@chromium.org271375b2010-06-23 19:17:38 +0000243 deps_content = gclient_utils.FileRead(filepath)
maruel@chromium.org0d425922010-06-21 19:22:24 +0000244
maruel@chromium.org271375b2010-06-23 19:17:38 +0000245 # Eval the content.
246 # One thing is unintuitive, vars= {} must happen before Var() use.
247 local_scope = {}
248 var = self.VarImpl(self.custom_vars, local_scope)
249 global_scope = {
250 'File': self.FileImpl,
251 'From': self.FromImpl,
252 'Var': var.Lookup,
253 'deps_os': {},
254 }
maruel@chromium.org5990f9d2010-07-07 18:02:58 +0000255 try:
256 exec(deps_content, global_scope, local_scope)
257 except SyntaxError, e:
258 gclient_utils.SyntaxErrorToError(filepath, e)
maruel@chromium.org271375b2010-06-23 19:17:38 +0000259 deps = local_scope.get('deps', {})
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000260 # load os specific dependencies if defined. these dependencies may
261 # override or extend the values defined by the 'deps' member.
maruel@chromium.org271375b2010-06-23 19:17:38 +0000262 if 'deps_os' in local_scope:
263 for deps_os_key in self.enforced_os():
264 os_deps = local_scope['deps_os'].get(deps_os_key, {})
265 if len(self.enforced_os()) > 1:
266 # Ignore any conflict when including deps for more than one
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000267 # platform, so we collect the broadest set of dependencies available.
268 # We may end up with the wrong revision of something for our
269 # platform, but this is the best we can do.
270 deps.update([x for x in os_deps.items() if not x[0] in deps])
271 else:
272 deps.update(os_deps)
273
maruel@chromium.org271375b2010-06-23 19:17:38 +0000274 self.deps_hooks.extend(local_scope.get('hooks', []))
275
276 # If a line is in custom_deps, but not in the solution, we want to append
277 # this line to the solution.
278 for d in self.custom_deps:
279 if d not in deps:
280 deps[d] = self.custom_deps[d]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000281
282 # If use_relative_paths is set in the DEPS file, regenerate
283 # the dictionary using paths relative to the directory containing
284 # the DEPS file.
maruel@chromium.org271375b2010-06-23 19:17:38 +0000285 use_relative_paths = local_scope.get('use_relative_paths', False)
286 if use_relative_paths:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000287 rel_deps = {}
288 for d, url in deps.items():
289 # normpath is required to allow DEPS to use .. in their
290 # dependency local path.
maruel@chromium.org271375b2010-06-23 19:17:38 +0000291 rel_deps[os.path.normpath(os.path.join(self.name, d))] = url
292 deps = rel_deps
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000293
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000294 # Convert the deps into real Dependency.
295 for name, url in deps.iteritems():
296 if name in [s.name for s in self.dependencies]:
297 raise
298 self.dependencies.append(Dependency(self, name, url))
maruel@chromium.org85c2a192010-07-22 21:14:43 +0000299 # Note: do not sort by name, the dependencies must be specified in the
300 # logical order.
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000301 logging.info('Loaded: %s' % str(self))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000302
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000303 def RunCommandRecursively(self, options, revision_overrides,
304 command, args, pm):
305 """Runs 'command' before parsing the DEPS in case it's a initial checkout
306 or a revert."""
307 assert self.file_list == []
308 # When running runhooks, there's no need to consult the SCM.
309 # All known hooks are expected to run unconditionally regardless of working
310 # copy state, so skip the SCM status check.
311 run_scm = command not in ('runhooks', None)
312 self.LateOverride(self.url)
313 if run_scm and self.parsed_url:
314 if isinstance(self.parsed_url, self.FileImpl):
315 # Special support for single-file checkout.
316 if not command in (None, 'cleanup', 'diff', 'pack', 'status'):
317 options.revision = self.parsed_url.GetRevision()
318 scm = gclient_scm.SVNWrapper(self.parsed_url.GetPath(),
319 self.root_dir(),
320 self.name)
321 scm.RunCommand('updatesingle', options,
322 args + [self.parsed_url.GetFilename()],
323 self.file_list)
324 else:
325 options.revision = revision_overrides.get(self.name)
326 scm = gclient_scm.CreateSCM(self.parsed_url, self.root_dir(), self.name)
327 scm.RunCommand(command, options, args, self.file_list)
328 self.file_list = [os.path.join(self.name, f.strip())
329 for f in self.file_list]
330 options.revision = None
331 if pm:
332 # The + 1 comes from the fact that .gclient is considered a step in
333 # itself, .i.e. this code is called one time for the .gclient. This is not
334 # conceptually correct but it simplifies code.
335 pm._total = len(self.tree(False)) + 1
336 pm.update()
337 if self.recursion_limit():
338 # Then we can parse the DEPS file.
339 self.ParseDepsFile(True)
340 if pm:
341 pm._total = len(self.tree(False)) + 1
342 pm.update(0)
343 # Parse the dependencies of this dependency.
344 for s in self.dependencies:
345 # TODO(maruel): All these can run concurrently! No need for threads,
346 # just buffer stdout&stderr on pipes and flush as they complete.
347 # Watch out for stdin.
348 s.RunCommandRecursively(options, revision_overrides, command, args, pm)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000349
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000350 def RunHooksRecursively(self, options):
351 """Evaluates all hooks, running actions as needed. RunCommandRecursively()
352 must have been called before to load the DEPS."""
maruel@chromium.orgdc7445d2010-07-09 21:05:29 +0000353 # If "--force" was specified, run all hooks regardless of what files have
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000354 # changed.
355 if self.deps_hooks and self.direct_reference:
356 # TODO(maruel): If the user is using git or git-svn, then we don't know
357 # what files have changed so we always run all hooks. It'd be nice to fix
358 # that.
359 if (options.force or
360 isinstance(self.parsed_url, self.FileImpl) or
361 gclient_scm.GetScmName(self.parsed_url) in ('git', None) or
362 os.path.isdir(os.path.join(self.root_dir(), self.name, '.git'))):
363 for hook_dict in self.deps_hooks:
364 self._RunHookAction(hook_dict, [])
365 else:
366 # TODO(phajdan.jr): We should know exactly when the paths are absolute.
367 # Convert all absolute paths to relative.
368 for i in range(len(self.file_list)):
369 # It depends on the command being executed (like runhooks vs sync).
370 if not os.path.isabs(self.file_list[i]):
371 continue
maruel@chromium.orgdc7445d2010-07-09 21:05:29 +0000372
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000373 prefix = os.path.commonprefix([self.root_dir().lower(),
374 self.file_list[i].lower()])
375 self.file_list[i] = self.file_list[i][len(prefix):]
376
377 # Strip any leading path separators.
378 while (self.file_list[i].startswith('\\') or
379 self.file_list[i].startswith('/')):
380 self.file_list[i] = self.file_list[i][1:]
381
382 # Run hooks on the basis of whether the files from the gclient operation
383 # match each hook's pattern.
384 for hook_dict in self.deps_hooks:
385 pattern = re.compile(hook_dict['pattern'])
386 matching_file_list = [f for f in self.file_list if pattern.search(f)]
387 if matching_file_list:
388 self._RunHookAction(hook_dict, matching_file_list)
389 if self.recursion_limit():
390 for s in self.dependencies:
391 s.RunHooksRecursively(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000392
maruel@chromium.orgeaf61062010-07-07 18:42:39 +0000393 def _RunHookAction(self, hook_dict, matching_file_list):
394 """Runs the action from a single hook."""
395 logging.info(hook_dict)
396 logging.info(matching_file_list)
397 command = hook_dict['action'][:]
398 if command[0] == 'python':
399 # If the hook specified "python" as the first item, the action is a
400 # Python script. Run it by starting a new copy of the same
401 # interpreter.
402 command[0] = sys.executable
403
404 if '$matching_files' in command:
405 splice_index = command.index('$matching_files')
406 command[splice_index:splice_index + 1] = matching_file_list
407
408 # Use a discrete exit status code of 2 to indicate that a hook action
409 # failed. Users of this script may wish to treat hook action failures
410 # differently from VC failures.
411 return gclient_utils.SubprocessCall(command, self.root_dir(), fail_status=2)
412
maruel@chromium.org271375b2010-06-23 19:17:38 +0000413 def root_dir(self):
414 return self.parent.root_dir()
415
416 def enforced_os(self):
417 return self.parent.enforced_os()
418
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000419 def recursion_limit(self):
420 return self.parent.recursion_limit() - 1
421
422 def tree(self, force_all):
423 return self.parent.tree(force_all)
424
425 def get_custom_deps(self, name, url):
426 """Returns a custom deps if applicable."""
427 if self.parent:
428 url = self.parent.get_custom_deps(name, url)
429 # None is a valid return value to disable a dependency.
430 return self.custom_deps.get(name, url)
431
432 def __str__(self):
433 out = []
434 for i in ('name', 'url', 'safesync_url', 'custom_deps', 'custom_vars',
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000435 'deps_hooks', 'file_list'):
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000436 # 'deps_file'
437 if self.__dict__[i]:
438 out.append('%s: %s' % (i, self.__dict__[i]))
439
440 for d in self.dependencies:
441 out.extend([' ' + x for x in str(d).splitlines()])
442 out.append('')
443 return '\n'.join(out)
444
445 def __repr__(self):
446 return '%s: %s' % (self.name, self.url)
447
maruel@chromium.orgbffb9042010-07-22 20:59:36 +0000448 def hierarchy(self):
maruel@chromium.orgbc2d2f92010-07-22 21:26:48 +0000449 """Returns a human-readable hierarchical reference to a Dependency."""
maruel@chromium.orgbffb9042010-07-22 20:59:36 +0000450 out = '%s(%s)' % (self.name, self.url)
451 i = self.parent
452 while i and i.name:
453 out = '%s(%s) -> %s' % (i.name, i.url, out)
454 i = i.parent
455 return out
456
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000457
458class GClient(Dependency):
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000459 """Object that represent a gclient checkout. A tree of Dependency(), one per
460 solution or DEPS entry."""
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000461
462 DEPS_OS_CHOICES = {
463 "win32": "win",
464 "win": "win",
465 "cygwin": "win",
466 "darwin": "mac",
467 "mac": "mac",
468 "unix": "unix",
469 "linux": "unix",
470 "linux2": "unix",
471 }
472
473 DEFAULT_CLIENT_FILE_TEXT = ("""\
474solutions = [
475 { "name" : "%(solution_name)s",
476 "url" : "%(solution_url)s",
477 "custom_deps" : {
478 },
maruel@chromium.org73e21142010-07-05 13:32:01 +0000479 "safesync_url": "%(safesync_url)s",
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000480 },
481]
482""")
483
484 DEFAULT_SNAPSHOT_SOLUTION_TEXT = ("""\
485 { "name" : "%(solution_name)s",
486 "url" : "%(solution_url)s",
487 "custom_deps" : {
maruel@chromium.org73e21142010-07-05 13:32:01 +0000488%(solution_deps)s },
489 "safesync_url": "%(safesync_url)s",
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000490 },
491""")
492
493 DEFAULT_SNAPSHOT_FILE_TEXT = ("""\
494# Snapshot generated with gclient revinfo --snapshot
495solutions = [
maruel@chromium.org73e21142010-07-05 13:32:01 +0000496%(solution_list)s]
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000497""")
498
499 def __init__(self, root_dir, options):
500 Dependency.__init__(self, None, None, None)
maruel@chromium.org0d425922010-06-21 19:22:24 +0000501 self._options = options
maruel@chromium.org271375b2010-06-23 19:17:38 +0000502 if options.deps_os:
503 enforced_os = options.deps_os.split(',')
504 else:
505 enforced_os = [self.DEPS_OS_CHOICES.get(sys.platform, 'unix')]
506 if 'all' in enforced_os:
507 enforced_os = self.DEPS_OS_CHOICES.itervalues()
508 self._enforced_os = list(set(enforced_os))
509 self._root_dir = root_dir
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000510 self.config_content = None
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000511 # Do not change previous behavior. Only solution level and immediate DEPS
512 # are processed.
513 self._recursion_limit = 2
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000514
515 def SetConfig(self, content):
516 assert self.dependencies == []
517 config_dict = {}
518 self.config_content = content
519 try:
520 exec(content, config_dict)
521 except SyntaxError, e:
maruel@chromium.org5990f9d2010-07-07 18:02:58 +0000522 gclient_utils.SyntaxErrorToError('.gclient', e)
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000523 for s in config_dict.get('solutions', []):
maruel@chromium.org81843b82010-06-28 16:49:26 +0000524 try:
525 self.dependencies.append(Dependency(
526 self, s['name'], s['url'],
527 s.get('safesync_url', None),
528 s.get('custom_deps', {}),
529 s.get('custom_vars', {})))
530 except KeyError:
531 raise gclient_utils.Error('Invalid .gclient file. Solution is '
532 'incomplete: %s' % s)
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000533 # .gclient can have hooks.
534 self.deps_hooks = config_dict.get('hooks', [])
535
536 def SaveConfig(self):
537 gclient_utils.FileWrite(os.path.join(self.root_dir(),
538 self._options.config_filename),
539 self.config_content)
540
541 @staticmethod
542 def LoadCurrentConfig(options):
543 """Searches for and loads a .gclient file relative to the current working
544 dir. Returns a GClient object."""
545 path = gclient_utils.FindGclientRoot(os.getcwd(), options.config_filename)
546 if not path:
547 return None
548 client = GClient(path, options)
549 client.SetConfig(gclient_utils.FileRead(
550 os.path.join(path, options.config_filename)))
551 return client
552
553 def SetDefaultConfig(self, solution_name, solution_url, safesync_url):
554 self.SetConfig(self.DEFAULT_CLIENT_FILE_TEXT % {
555 'solution_name': solution_name,
556 'solution_url': solution_url,
557 'safesync_url' : safesync_url,
558 })
559
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000560 def _SaveEntries(self):
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000561 """Creates a .gclient_entries file to record the list of unique checkouts.
562
563 The .gclient_entries file lives in the same directory as .gclient.
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000564 """
565 # Sometimes pprint.pformat will use {', sometimes it'll use { ' ... It
566 # makes testing a bit too fun.
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000567 result = 'entries = {\n'
568 for entry in self.tree(False):
569 # Skip over File() dependencies as we can't version them.
570 if not isinstance(entry.parsed_url, self.FileImpl):
571 result += ' %s: %s,\n' % (pprint.pformat(entry.name),
572 pprint.pformat(entry.parsed_url))
573 result += '}\n'
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000574 file_path = os.path.join(self.root_dir(), self._options.entries_filename)
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000575 logging.info(result)
576 gclient_utils.FileWrite(file_path, result)
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000577
578 def _ReadEntries(self):
579 """Read the .gclient_entries file for the given client.
580
581 Returns:
582 A sequence of solution names, which will be empty if there is the
583 entries file hasn't been created yet.
584 """
585 scope = {}
586 filename = os.path.join(self.root_dir(), self._options.entries_filename)
587 if not os.path.exists(filename):
maruel@chromium.org73e21142010-07-05 13:32:01 +0000588 return {}
maruel@chromium.org5990f9d2010-07-07 18:02:58 +0000589 try:
590 exec(gclient_utils.FileRead(filename), scope)
591 except SyntaxError, e:
592 gclient_utils.SyntaxErrorToError(filename, e)
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000593 return scope['entries']
594
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000595 def _EnforceRevisions(self):
maruel@chromium.org918a9ae2010-05-28 15:50:30 +0000596 """Checks for revision overrides."""
597 revision_overrides = {}
maruel@chromium.org307d1792010-05-31 20:03:13 +0000598 if self._options.head:
599 return revision_overrides
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000600 for s in self.dependencies:
601 if not s.safesync_url:
maruel@chromium.org307d1792010-05-31 20:03:13 +0000602 continue
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000603 handle = urllib.urlopen(s.safesync_url)
maruel@chromium.org307d1792010-05-31 20:03:13 +0000604 rev = handle.read().strip()
605 handle.close()
606 if len(rev):
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000607 self._options.revisions.append('%s@%s' % (s.name, rev))
maruel@chromium.org307d1792010-05-31 20:03:13 +0000608 if not self._options.revisions:
609 return revision_overrides
610 # --revision will take over safesync_url.
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000611 solutions_names = [s.name for s in self.dependencies]
maruel@chromium.org307d1792010-05-31 20:03:13 +0000612 index = 0
613 for revision in self._options.revisions:
614 if not '@' in revision:
615 # Support for --revision 123
616 revision = '%s@%s' % (solutions_names[index], revision)
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000617 sol, rev = revision.split('@', 1)
maruel@chromium.org307d1792010-05-31 20:03:13 +0000618 if not sol in solutions_names:
619 #raise gclient_utils.Error('%s is not a valid solution.' % sol)
620 print >> sys.stderr, ('Please fix your script, having invalid '
621 '--revision flags will soon considered an error.')
622 else:
maruel@chromium.org918a9ae2010-05-28 15:50:30 +0000623 revision_overrides[sol] = rev
maruel@chromium.org307d1792010-05-31 20:03:13 +0000624 index += 1
maruel@chromium.org918a9ae2010-05-28 15:50:30 +0000625 return revision_overrides
626
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000627 def RunOnDeps(self, command, args):
628 """Runs a command on each dependency in a client and its dependencies.
629
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000630 Args:
631 command: The command to use (e.g., 'status' or 'diff')
632 args: list of str - extra arguments to add to the command line.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000633 """
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000634 if not self.dependencies:
maruel@chromium.org73e21142010-07-05 13:32:01 +0000635 raise gclient_utils.Error('No solution specified')
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000636 revision_overrides = self._EnforceRevisions()
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000637 pm = None
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000638 if command == 'update' and not self._options.verbose:
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000639 pm = Progress('Syncing projects', len(self.tree(False)) + 1)
640 self.RunCommandRecursively(self._options, revision_overrides,
641 command, args, pm)
642 if pm:
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000643 pm.end()
piman@chromium.org6f363722010-04-27 00:41:09 +0000644
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000645 # Once all the dependencies have been processed, it's now safe to run the
646 # hooks.
647 if not self._options.nohooks:
648 self.RunHooksRecursively(self._options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000649
650 if command == 'update':
ajwong@chromium.orgcdcee802009-06-23 15:30:42 +0000651 # Notify the user if there is an orphaned entry in their working copy.
652 # Only delete the directory if there are no changes in it, and
653 # delete_unversioned_trees is set to true.
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000654 entries = [i.name for i in self.tree(False)]
655 for entry, prev_url in self._ReadEntries().iteritems():
maruel@chromium.orgc5e9aec2009-08-03 18:25:56 +0000656 # Fix path separator on Windows.
657 entry_fixed = entry.replace('/', os.path.sep)
maruel@chromium.org75a59272010-06-11 22:34:03 +0000658 e_dir = os.path.join(self.root_dir(), entry_fixed)
maruel@chromium.orgc5e9aec2009-08-03 18:25:56 +0000659 # Use entry and not entry_fixed there.
maruel@chromium.org0329e672009-05-13 18:41:04 +0000660 if entry not in entries and os.path.exists(e_dir):
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000661 file_list = []
662 scm = gclient_scm.CreateSCM(prev_url, self.root_dir(), entry_fixed)
663 scm.status(self._options, [], file_list)
664 modified_files = file_list != []
msb@chromium.org83017012009-09-28 18:52:12 +0000665 if not self._options.delete_unversioned_trees or modified_files:
maruel@chromium.orgc5e9aec2009-08-03 18:25:56 +0000666 # There are modified files in this entry. Keep warning until
667 # removed.
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000668 print(('\nWARNING: \'%s\' is no longer part of this client. '
669 'It is recommended that you manually remove it.\n') %
maruel@chromium.orgc5e9aec2009-08-03 18:25:56 +0000670 entry_fixed)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000671 else:
672 # Delete the entry
maruel@chromium.org73e21142010-07-05 13:32:01 +0000673 print('\n________ deleting \'%s\' in \'%s\'' % (
674 entry_fixed, self.root_dir()))
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000675 gclient_utils.RemoveDirectory(e_dir)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000676 # record the current list of entries for next time
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000677 self._SaveEntries()
maruel@chromium.org17cdf762010-05-28 17:30:52 +0000678 return 0
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000679
680 def PrintRevInfo(self):
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000681 """Output revision info mapping for the client and its dependencies.
682
683 This allows the capture of an overall "revision" for the source tree that
maruel@chromium.org918a9ae2010-05-28 15:50:30 +0000684 can be used to reproduce the same tree in the future. It is only useful for
685 "unpinned dependencies", i.e. DEPS/deps references without a svn revision
686 number or a git hash. A git branch name isn't "pinned" since the actual
687 commit can change.
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000688
689 The --snapshot option allows creating a .gclient file to reproduce the tree.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000690 """
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000691 if not self.dependencies:
maruel@chromium.org73e21142010-07-05 13:32:01 +0000692 raise gclient_utils.Error('No solution specified')
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000693 # Load all the settings.
694 self.RunCommandRecursively(self._options, {}, None, [], None)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000695
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000696 def GetURLAndRev(name, original_url):
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000697 """Returns the revision-qualified SCM url."""
698 if original_url is None:
maruel@chromium.orgbaa578e2010-07-12 17:36:59 +0000699 return None
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000700 if isinstance(original_url, self.FileImpl):
701 original_url = original_url.file_location
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000702 url, _ = gclient_utils.SplitUrlRevision(original_url)
maruel@chromium.org75a59272010-06-11 22:34:03 +0000703 scm = gclient_scm.CreateSCM(original_url, self.root_dir(), name)
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000704 if not os.path.isdir(scm.checkout_path):
705 return None
maruel@chromium.orgbaa578e2010-07-12 17:36:59 +0000706 return '%s@%s' % (url, scm.revinfo(self._options, [], None))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000707
maruel@chromium.orgbaa578e2010-07-12 17:36:59 +0000708 if self._options.snapshot:
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000709 new_gclient = ''
710 # First level at .gclient
711 for d in self.dependencies:
712 entries = {}
713 def GrabDeps(sol):
714 """Recursively grab dependencies."""
715 for i in sol.dependencies:
716 entries[i.name] = GetURLAndRev(i.name, i.parsed_url)
717 GrabDeps(i)
718 GrabDeps(d)
719 custom_deps = []
720 for k in sorted(entries.keys()):
721 if entries[k]:
722 # Quotes aren't escaped...
723 custom_deps.append(' \"%s\": \'%s\',\n' % (k, entries[k]))
724 else:
725 custom_deps.append(' \"%s\": None,\n' % k)
726 new_gclient += self.DEFAULT_SNAPSHOT_SOLUTION_TEXT % {
727 'solution_name': d.name,
728 'solution_url': d.url,
729 'safesync_url' : d.safesync_url or '',
730 'solution_deps': ''.join(custom_deps),
731 }
732 # Print the snapshot configuration file
733 print(self.DEFAULT_SNAPSHOT_FILE_TEXT % {'solution_list': new_gclient})
nasser@codeaurora.orgde8f3522010-03-11 23:47:44 +0000734 else:
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000735 entries = sorted(self.tree(False), key=lambda i: i.name)
736 for entry in entries:
737 entry_url = GetURLAndRev(entry.name, entry.parsed_url)
738 line = '%s: %s' % (entry.name, entry_url)
739 if not entry is entries[-1]:
740 line += ';'
741 print line
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000742
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000743 def ParseDepsFile(self, direct_reference):
744 """No DEPS to parse for a .gclient file."""
745 self.direct_reference = direct_reference
746 self.deps_parsed = True
747
maruel@chromium.org75a59272010-06-11 22:34:03 +0000748 def root_dir(self):
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000749 """Root directory of gclient checkout."""
maruel@chromium.org75a59272010-06-11 22:34:03 +0000750 return self._root_dir
751
maruel@chromium.org271375b2010-06-23 19:17:38 +0000752 def enforced_os(self):
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000753 """What deps_os entries that are to be parsed."""
maruel@chromium.org271375b2010-06-23 19:17:38 +0000754 return self._enforced_os
755
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000756 def recursion_limit(self):
757 """How recursive can each dependencies in DEPS file can load DEPS file."""
758 return self._recursion_limit
759
760 def tree(self, force_all):
761 """Returns a flat list of all the dependencies."""
762 def subtree(dep):
763 if not force_all and not dep.direct_reference:
764 # Was loaded from a From() keyword in a DEPS file, don't load all its
765 # dependencies.
766 return []
767 result = dep.dependencies[:]
768 for d in dep.dependencies:
769 result.extend(subtree(d))
770 return result
771 return subtree(self)
772
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000773
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000774#### gclient commands.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000775
776
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000777def CMDcleanup(parser, args):
778 """Cleans up all working copies.
maruel@chromium.orgddff62d2010-05-17 21:02:36 +0000779
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000780Mostly svn-specific. Simply runs 'svn cleanup' for each module.
maruel@chromium.org79692d62010-05-14 18:57:13 +0000781"""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000782 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
783 help='override deps for the specified (comma-separated) '
784 'platform(s); \'all\' will process all deps_os '
785 'references')
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000786 (options, args) = parser.parse_args(args)
maruel@chromium.org2806acc2009-05-15 12:33:34 +0000787 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000788 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000789 raise gclient_utils.Error('client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000790 if options.verbose:
791 # Print out the .gclient file. This is longer than if we just printed the
792 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.org116704f2010-06-11 17:34:38 +0000793 print(client.config_content)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000794 return client.RunOnDeps('cleanup', args)
795
796
piman@chromium.org4b90e3a2010-07-01 20:28:26 +0000797@attr('usage', '[command] [args ...]')
798def CMDrecurse(parser, args):
799 """Operates on all the entries.
800
801 Runs a shell command on all entries.
802 """
803 # Stop parsing at the first non-arg so that these go through to the command
804 parser.disable_interspersed_args()
805 parser.add_option('-s', '--scm', action='append', default=[],
806 help='choose scm types to operate upon')
807 options, args = parser.parse_args(args)
808 root, entries = gclient_utils.GetGClientRootAndEntries()
809 scm_set = set()
810 for scm in options.scm:
811 scm_set.update(scm.split(','))
812
813 # Pass in the SCM type as an env variable
814 env = os.environ.copy()
815
816 for path, url in entries.iteritems():
817 scm = gclient_scm.GetScmName(url)
818 if scm_set and scm not in scm_set:
819 continue
820 dir = os.path.normpath(os.path.join(root, path))
821 env['GCLIENT_SCM'] = scm
822 env['GCLIENT_URL'] = url
823 subprocess.Popen(args, cwd=dir, env=env).communicate()
824
825
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000826@attr('usage', '[url] [safesync url]')
827def CMDconfig(parser, args):
maruel@chromium.orgddff62d2010-05-17 21:02:36 +0000828 """Create a .gclient file in the current directory.
829
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000830This specifies the configuration for further commands. After update/sync,
maruel@chromium.org79692d62010-05-14 18:57:13 +0000831top-level DEPS files in each module are read to determine dependent
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000832modules to operate on as well. If optional [url] parameter is
maruel@chromium.org79692d62010-05-14 18:57:13 +0000833provided, then configuration is read from a specified Subversion server
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000834URL.
maruel@chromium.org79692d62010-05-14 18:57:13 +0000835"""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000836 parser.add_option('--spec',
837 help='create a gclient file containing the provided '
838 'string. Due to Cygwin/Python brokenness, it '
839 'probably can\'t contain any newlines.')
840 parser.add_option('--name',
841 help='overrides the default name for the solution')
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000842 (options, args) = parser.parse_args(args)
maruel@chromium.org5fc2a332010-05-26 19:37:15 +0000843 if ((options.spec and args) or len(args) > 2 or
844 (not options.spec and not args)):
845 parser.error('Inconsistent arguments. Use either --spec or one or 2 args')
846
maruel@chromium.org0329e672009-05-13 18:41:04 +0000847 if os.path.exists(options.config_filename):
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000848 raise gclient_utils.Error('%s file already exists in the current directory'
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000849 % options.config_filename)
maruel@chromium.org2806acc2009-05-15 12:33:34 +0000850 client = GClient('.', options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000851 if options.spec:
852 client.SetConfig(options.spec)
853 else:
maruel@chromium.org1ab7ffc2009-06-03 17:21:37 +0000854 base_url = args[0].rstrip('/')
iposva@chromium.org8cf7a392010-04-07 17:20:26 +0000855 if not options.name:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000856 name = base_url.split('/')[-1]
iposva@chromium.org8cf7a392010-04-07 17:20:26 +0000857 else:
858 # specify an alternate relpath for the given URL.
859 name = options.name
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000860 safesync_url = ''
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000861 if len(args) > 1:
862 safesync_url = args[1]
863 client.SetDefaultConfig(name, base_url, safesync_url)
864 client.SaveConfig()
maruel@chromium.org79692d62010-05-14 18:57:13 +0000865 return 0
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000866
867
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000868def CMDexport(parser, args):
maruel@chromium.orgddff62d2010-05-17 21:02:36 +0000869 """Wrapper for svn export for all managed directories."""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000870 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
871 help='override deps for the specified (comma-separated) '
872 'platform(s); \'all\' will process all deps_os '
873 'references')
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000874 (options, args) = parser.parse_args(args)
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +0000875 if len(args) != 1:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000876 raise gclient_utils.Error('Need directory name')
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +0000877 client = GClient.LoadCurrentConfig(options)
878
879 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000880 raise gclient_utils.Error('client not configured; see \'gclient config\'')
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +0000881
882 if options.verbose:
883 # Print out the .gclient file. This is longer than if we just printed the
884 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.org116704f2010-06-11 17:34:38 +0000885 print(client.config_content)
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +0000886 return client.RunOnDeps('export', args)
887
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000888
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000889@attr('epilog', """Example:
890 gclient pack > patch.txt
891 generate simple patch for configured client and dependences
892""")
893def CMDpack(parser, args):
maruel@chromium.org79692d62010-05-14 18:57:13 +0000894 """Generate a patch which can be applied at the root of the tree.
maruel@chromium.orgddff62d2010-05-17 21:02:36 +0000895
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000896Internally, runs 'svn diff'/'git diff' on each checked out module and
maruel@chromium.org79692d62010-05-14 18:57:13 +0000897dependencies, and performs minimal postprocessing of the output. The
898resulting patch is printed to stdout and can be applied to a freshly
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000899checked out tree via 'patch -p0 < patchfile'.
maruel@chromium.org79692d62010-05-14 18:57:13 +0000900"""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000901 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
902 help='override deps for the specified (comma-separated) '
903 'platform(s); \'all\' will process all deps_os '
904 'references')
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000905 (options, args) = parser.parse_args(args)
kbr@google.comab318592009-09-04 00:54:55 +0000906 client = GClient.LoadCurrentConfig(options)
907 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000908 raise gclient_utils.Error('client not configured; see \'gclient config\'')
kbr@google.comab318592009-09-04 00:54:55 +0000909 if options.verbose:
910 # Print out the .gclient file. This is longer than if we just printed the
911 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.org116704f2010-06-11 17:34:38 +0000912 print(client.config_content)
kbr@google.comab318592009-09-04 00:54:55 +0000913 return client.RunOnDeps('pack', args)
914
915
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000916def CMDstatus(parser, args):
917 """Show modification status for every dependencies."""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000918 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
919 help='override deps for the specified (comma-separated) '
920 'platform(s); \'all\' will process all deps_os '
921 'references')
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000922 (options, args) = parser.parse_args(args)
maruel@chromium.org2806acc2009-05-15 12:33:34 +0000923 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000924 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000925 raise gclient_utils.Error('client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000926 if options.verbose:
927 # Print out the .gclient file. This is longer than if we just printed the
928 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.org116704f2010-06-11 17:34:38 +0000929 print(client.config_content)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000930 return client.RunOnDeps('status', args)
931
932
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000933@attr('epilog', """Examples:
maruel@chromium.org79692d62010-05-14 18:57:13 +0000934 gclient sync
935 update files from SCM according to current configuration,
936 *for modules which have changed since last update or sync*
937 gclient sync --force
938 update files from SCM according to current configuration, for
939 all modules (useful for recovering files deleted from local copy)
940 gclient sync --revision src@31000
941 update src directory to r31000
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000942""")
943def CMDsync(parser, args):
944 """Checkout/update all modules."""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000945 parser.add_option('-f', '--force', action='store_true',
946 help='force update even for unchanged modules')
947 parser.add_option('-n', '--nohooks', action='store_true',
948 help='don\'t run hooks after the update is complete')
949 parser.add_option('-r', '--revision', action='append',
950 dest='revisions', metavar='REV', default=[],
951 help='Enforces revision/hash for the solutions with the '
952 'format src@rev. The src@ part is optional and can be '
953 'skipped. -r can be used multiple times when .gclient '
954 'has multiple solutions configured and will work even '
955 'if the src@ part is skipped.')
956 parser.add_option('-H', '--head', action='store_true',
957 help='skips any safesync_urls specified in '
958 'configured solutions and sync to head instead')
959 parser.add_option('-D', '--delete_unversioned_trees', action='store_true',
960 help='delete any unexpected unversioned trees '
961 'that are in the checkout')
962 parser.add_option('-R', '--reset', action='store_true',
963 help='resets any local changes before updating (git only)')
964 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
965 help='override deps for the specified (comma-separated) '
966 'platform(s); \'all\' will process all deps_os '
967 'references')
968 parser.add_option('-m', '--manually_grab_svn_rev', action='store_true',
969 help='Skip svn up whenever possible by requesting '
970 'actual HEAD revision from the repository')
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000971 (options, args) = parser.parse_args(args)
maruel@chromium.org2806acc2009-05-15 12:33:34 +0000972 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000973
974 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000975 raise gclient_utils.Error('client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000976
maruel@chromium.org307d1792010-05-31 20:03:13 +0000977 if options.revisions and options.head:
978 # TODO(maruel): Make it a parser.error if it doesn't break any builder.
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000979 print('Warning: you cannot use both --head and --revision')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000980
981 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('update', args)
986
987
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000988def CMDupdate(parser, args):
maruel@chromium.orgddff62d2010-05-17 21:02:36 +0000989 """Alias for the sync command. Deprecated."""
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000990 return CMDsync(parser, args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000991
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000992def CMDdiff(parser, args):
993 """Displays local diff for every dependencies."""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000994 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
995 help='override deps for the specified (comma-separated) '
996 'platform(s); \'all\' will process all deps_os '
997 'references')
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000998 (options, args) = parser.parse_args(args)
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 if options.verbose:
1003 # Print out the .gclient file. This is longer than if we just printed the
1004 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.org116704f2010-06-11 17:34:38 +00001005 print(client.config_content)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001006 return client.RunOnDeps('diff', args)
1007
1008
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001009def CMDrevert(parser, args):
1010 """Revert all modifications in every dependencies."""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00001011 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
1012 help='override deps for the specified (comma-separated) '
1013 'platform(s); \'all\' will process all deps_os '
1014 'references')
1015 parser.add_option('-n', '--nohooks', action='store_true',
1016 help='don\'t run hooks after the revert is complete')
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001017 (options, args) = parser.parse_args(args)
1018 # --force is implied.
1019 options.force = True
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001020 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001021 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00001022 raise gclient_utils.Error('client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001023 return client.RunOnDeps('revert', args)
1024
1025
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001026def CMDrunhooks(parser, args):
1027 """Runs hooks for files that have been modified in the local working copy."""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00001028 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
1029 help='override deps for the specified (comma-separated) '
1030 'platform(s); \'all\' will process all deps_os '
1031 'references')
1032 parser.add_option('-f', '--force', action='store_true', default=True,
1033 help='Deprecated. No effect.')
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001034 (options, args) = parser.parse_args(args)
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001035 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001036 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00001037 raise gclient_utils.Error('client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001038 if options.verbose:
1039 # Print out the .gclient file. This is longer than if we just printed the
1040 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.org116704f2010-06-11 17:34:38 +00001041 print(client.config_content)
maruel@chromium.org5df6a462009-08-28 18:52:26 +00001042 options.force = True
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001043 options.nohooks = False
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001044 return client.RunOnDeps('runhooks', args)
1045
1046
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001047def CMDrevinfo(parser, args):
maruel@chromium.org9eda4112010-06-11 18:56:10 +00001048 """Output revision info mapping for the client and its dependencies.
1049
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00001050 This allows the capture of an overall 'revision' for the source tree that
maruel@chromium.org9eda4112010-06-11 18:56:10 +00001051 can be used to reproduce the same tree in the future. It is only useful for
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00001052 'unpinned dependencies', i.e. DEPS/deps references without a svn revision
1053 number or a git hash. A git branch name isn't 'pinned' since the actual
maruel@chromium.org9eda4112010-06-11 18:56:10 +00001054 commit can change.
1055 """
1056 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
1057 help='override deps for the specified (comma-separated) '
1058 'platform(s); \'all\' will process all deps_os '
1059 'references')
1060 parser.add_option('-s', '--snapshot', action='store_true',
1061 help='creates a snapshot .gclient file of the current '
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +00001062 'version of all repositories to reproduce the tree')
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001063 (options, args) = parser.parse_args(args)
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001064 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001065 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00001066 raise gclient_utils.Error('client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001067 client.PrintRevInfo()
maruel@chromium.org79692d62010-05-14 18:57:13 +00001068 return 0
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001069
1070
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001071def Command(name):
1072 return getattr(sys.modules[__name__], 'CMD' + name, None)
1073
1074
1075def CMDhelp(parser, args):
1076 """Prints list of commands or help for a specific command."""
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001077 (_, args) = parser.parse_args(args)
maruel@chromium.orgddff62d2010-05-17 21:02:36 +00001078 if len(args) == 1:
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001079 return Main(args + ['--help'])
maruel@chromium.orgddff62d2010-05-17 21:02:36 +00001080 parser.print_help()
1081 return 0
1082
1083
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001084def GenUsage(parser, command):
1085 """Modify an OptParse object with the function's documentation."""
1086 obj = Command(command)
1087 if command == 'help':
1088 command = '<command>'
1089 # OptParser.description prefer nicely non-formatted strings.
1090 parser.description = re.sub('[\r\n ]{2,}', ' ', obj.__doc__)
1091 usage = getattr(obj, 'usage', '')
1092 parser.set_usage('%%prog %s [options] %s' % (command, usage))
1093 parser.epilog = getattr(obj, 'epilog', None)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001094
1095
1096def Main(argv):
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001097 """Doesn't parse the arguments here, just find the right subcommand to
1098 execute."""
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001099 try:
1100 # Do it late so all commands are listed.
1101 CMDhelp.usage = ('\n\nCommands are:\n' + '\n'.join([
1102 ' %-10s %s' % (fn[3:], Command(fn[3:]).__doc__.split('\n')[0].strip())
1103 for fn in dir(sys.modules[__name__]) if fn.startswith('CMD')]))
1104 parser = optparse.OptionParser(version='%prog ' + __version__)
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001105 parser.add_option('-v', '--verbose', action='count', default=0,
1106 help='Produces additional output for diagnostics. Can be '
1107 'used up to three times for more logging info.')
1108 parser.add_option('--gclientfile', dest='config_filename',
1109 default=os.environ.get('GCLIENT_FILE', '.gclient'),
1110 help='Specify an alternate %default file')
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001111 # Integrate standard options processing.
1112 old_parser = parser.parse_args
1113 def Parse(args):
1114 (options, args) = old_parser(args)
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001115 level = None
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001116 if options.verbose == 2:
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001117 level = logging.INFO
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001118 elif options.verbose > 2:
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001119 level = logging.DEBUG
1120 logging.basicConfig(level=level,
1121 format='%(module)s(%(lineno)d) %(funcName)s:%(message)s')
1122 options.entries_filename = options.config_filename + '_entries'
maruel@chromium.orge3216c62010-07-08 03:31:43 +00001123
1124 # These hacks need to die.
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001125 if not hasattr(options, 'revisions'):
1126 # GClient.RunOnDeps expects it even if not applicable.
1127 options.revisions = []
1128 if not hasattr(options, 'head'):
1129 options.head = None
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001130 if not hasattr(options, 'nohooks'):
1131 options.nohooks = True
1132 if not hasattr(options, 'deps_os'):
1133 options.deps_os = None
maruel@chromium.orge3216c62010-07-08 03:31:43 +00001134 if not hasattr(options, 'manually_grab_svn_rev'):
1135 options.manually_grab_svn_rev = None
1136 if not hasattr(options, 'force'):
1137 options.force = None
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001138 return (options, args)
1139 parser.parse_args = Parse
1140 # We don't want wordwrapping in epilog (usually examples)
1141 parser.format_epilog = lambda _: parser.epilog or ''
1142 if argv:
1143 command = Command(argv[0])
1144 if command:
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001145 # 'fix' the usage and the description now that we know the subcommand.
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001146 GenUsage(parser, argv[0])
1147 return command(parser, argv[1:])
1148 # Not a known command. Default to help.
1149 GenUsage(parser, 'help')
1150 return CMDhelp(parser, argv)
1151 except gclient_utils.Error, e:
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001152 print >> sys.stderr, 'Error: %s' % str(e)
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001153 return 1
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001154
1155
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001156if '__main__' == __name__:
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001157 sys.exit(Main(sys.argv[1:]))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001158
1159# vim: ts=2:sw=2:tw=80:et: