blob: 42a6938dd0c287836f24459eda8b508a9cf95e93 [file] [log] [blame]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001#!/usr/bin/python
maruel@chromium.orgba551772010-02-03 18:21:42 +00002# Copyright (c) 2010 The Chromium Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00005
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00006"""Meta checkout manager supporting both Subversion and GIT.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00007
8Files
9 .gclient : Current client configuration, written by 'config' command.
10 Format is a Python script defining 'solutions', a list whose
11 entries each are maps binding the strings "name" and "url"
12 to strings specifying the name and location of the client
13 module, as well as "custom_deps" to a map similar to the DEPS
14 file below.
15 .gclient_entries : A cache constructed by 'update' command. Format is a
16 Python script defining 'entries', a list of the names
17 of all modules in the client
18 <module>/DEPS : Python script defining var 'deps' as a map from each requisite
19 submodule name to a URL where it can be found (via one SCM)
20
21Hooks
22 .gclient and DEPS files may optionally contain a list named "hooks" to
23 allow custom actions to be performed based on files that have changed in the
evan@chromium.org67820ef2009-07-27 17:23:00 +000024 working copy as a result of a "sync"/"update" or "revert" operation. This
maruel@chromium.org0b6a0842010-06-15 14:34:19 +000025 can be prevented by using --nohooks (hooks run by default). Hooks can also
maruel@chromium.org5df6a462009-08-28 18:52:26 +000026 be forced to run with the "runhooks" operation. If "sync" is run with
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000027 --force, all known hooks will run regardless of the state of the working
28 copy.
29
30 Each item in a "hooks" list is a dict, containing these two keys:
31 "pattern" The associated value is a string containing a regular
32 expression. When a file whose pathname matches the expression
33 is checked out, updated, or reverted, the hook's "action" will
34 run.
35 "action" A list describing a command to run along with its arguments, if
36 any. An action command will run at most one time per gclient
37 invocation, regardless of how many files matched the pattern.
38 The action is executed in the same directory as the .gclient
39 file. If the first item in the list is the string "python",
40 the current Python interpreter (sys.executable) will be used
phajdan.jr@chromium.org71b40682009-07-31 23:40:09 +000041 to run the command. If the list contains string "$matching_files"
42 it will be removed from the list and the list will be extended
43 by the list of matching files.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000044
45 Example:
46 hooks = [
47 { "pattern": "\\.(gif|jpe?g|pr0n|png)$",
48 "action": ["python", "image_indexer.py", "--all"]},
49 ]
50"""
51
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +000052__version__ = "0.5"
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000053
maruel@chromium.org754960e2009-09-21 12:31:05 +000054import logging
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000055import optparse
56import os
msb@chromium.org2e38de72009-09-28 17:04:47 +000057import pprint
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000058import re
piman@chromium.org4b90e3a2010-07-01 20:28:26 +000059import subprocess
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000060import sys
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000061import urlparse
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000062import urllib
63
maruel@chromium.orgada4c652009-12-03 15:32:01 +000064import breakpad
65
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000066import gclient_scm
67import gclient_utils
nasser@codeaurora.org1f7a3d12010-02-04 15:11:50 +000068from third_party.repo.progress import Progress
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000069
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000070
maruel@chromium.org1f7d1182010-05-17 18:17:38 +000071def attr(attr, data):
72 """Sets an attribute on a function."""
73 def hook(fn):
74 setattr(fn, attr, data)
75 return fn
76 return hook
maruel@chromium.orge3da35f2010-03-09 21:40:45 +000077
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000078
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000079## GClient implementation.
80
81
maruel@chromium.org116704f2010-06-11 17:34:38 +000082class GClientKeywords(object):
83 class FromImpl(object):
84 """Used to implement the From() syntax."""
85
86 def __init__(self, module_name, sub_target_name=None):
87 """module_name is the dep module we want to include from. It can also be
88 the name of a subdirectory to include from.
89
90 sub_target_name is an optional parameter if the module name in the other
91 DEPS file is different. E.g., you might want to map src/net to net."""
92 self.module_name = module_name
93 self.sub_target_name = sub_target_name
94
95 def __str__(self):
96 return 'From(%s, %s)' % (repr(self.module_name),
97 repr(self.sub_target_name))
98
maruel@chromium.org116704f2010-06-11 17:34:38 +000099 class FileImpl(object):
100 """Used to implement the File('') syntax which lets you sync a single file
maruel@chromium.orge3216c62010-07-08 03:31:43 +0000101 from a SVN repo."""
maruel@chromium.org116704f2010-06-11 17:34:38 +0000102
103 def __init__(self, file_location):
104 self.file_location = file_location
105
106 def __str__(self):
107 return 'File("%s")' % self.file_location
108
109 def GetPath(self):
110 return os.path.split(self.file_location)[0]
111
112 def GetFilename(self):
113 rev_tokens = self.file_location.split('@')
114 return os.path.split(rev_tokens[0])[1]
115
116 def GetRevision(self):
117 rev_tokens = self.file_location.split('@')
118 if len(rev_tokens) > 1:
119 return rev_tokens[1]
120 return None
121
122 class VarImpl(object):
123 def __init__(self, custom_vars, local_scope):
124 self._custom_vars = custom_vars
125 self._local_scope = local_scope
126
127 def Lookup(self, var_name):
128 """Implements the Var syntax."""
129 if var_name in self._custom_vars:
130 return self._custom_vars[var_name]
131 elif var_name in self._local_scope.get("vars", {}):
132 return self._local_scope["vars"][var_name]
133 raise gclient_utils.Error("Var is not defined: %s" % var_name)
134
135
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000136class Dependency(GClientKeywords):
137 """Object that represents a dependency checkout."""
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000138 DEPS_FILE = 'DEPS'
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000139
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000140 def __init__(self, parent, name, url, safesync_url=None, custom_deps=None,
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000141 custom_vars=None, deps_file=None):
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000142 GClientKeywords.__init__(self)
143 self.parent = parent
144 self.name = name
145 self.url = url
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000146 self.parsed_url = None
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000147 # These 2 are only set in .gclient and not in DEPS files.
148 self.safesync_url = safesync_url
149 self.custom_vars = custom_vars or {}
150 self.custom_deps = custom_deps or {}
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000151 self.deps_hooks = []
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000152 self.dependencies = []
153 self.deps_file = deps_file or self.DEPS_FILE
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000154 # A cache of the files affected by the current operation, necessary for
155 # hooks.
156 self.file_list = []
maruel@chromium.org271375b2010-06-23 19:17:38 +0000157 self.deps_parsed = False
158 self.direct_reference = False
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000159
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000160 # Sanity checks
161 if not self.name and self.parent:
162 raise gclient_utils.Error('Dependency without name')
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000163 if self.name in [d.name for d in self.tree(False)]:
164 raise gclient_utils.Error('Dependency %s specified more than once' %
165 self.name)
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000166 if not isinstance(self.url,
167 (basestring, self.FromImpl, self.FileImpl, None.__class__)):
168 raise gclient_utils.Error('dependency url must be either a string, None, '
169 'File() or From() instead of %s' %
170 self.url.__class__.__name__)
171 if '/' in self.deps_file or '\\' in self.deps_file:
172 raise gclient_utils.Error('deps_file name must not be a path, just a '
173 'filename. %s' % self.deps_file)
174
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000175 def LateOverride(self, url):
176 overriden_url = self.get_custom_deps(self.name, url)
177 if overriden_url != url:
178 self.parsed_url = overriden_url
179 logging.debug('%s, %s was overriden to %s' % (self.name, url,
180 self.parsed_url))
181 elif isinstance(url, self.FromImpl):
182 ref = [dep for dep in self.tree(True) if url.module_name == dep.name]
183 if not len(ref) == 1:
184 raise Exception('Failed to find one reference to %s. %s' % (
185 url.module_name, ref))
186 ref = ref[0]
187 sub_target = url.sub_target_name or url
188 # Make sure the referenced dependency DEPS file is loaded and file the
189 # inner referenced dependency.
190 ref.ParseDepsFile(False)
191 found_dep = None
192 for d in ref.dependencies:
193 if d.name == sub_target:
194 found_dep = d
195 break
196 if not found_dep:
197 raise Exception('Couldn\'t find %s in %s, referenced by %s' % (
198 sub_target, ref.name, self.name))
199 # Call LateOverride() again.
200 self.parsed_url = found_dep.LateOverride(found_dep.url)
201 logging.debug('%s, %s to %s' % (self.name, url, self.parsed_url))
202 elif isinstance(url, basestring):
203 parsed_url = urlparse.urlparse(url)
204 if not parsed_url[0]:
205 # A relative url. Fetch the real base.
206 path = parsed_url[2]
207 if not path.startswith('/'):
208 raise gclient_utils.Error(
209 'relative DEPS entry \'%s\' must begin with a slash' % url)
210 # Create a scm just to query the full url.
211 parent_url = self.parent.parsed_url
212 if isinstance(parent_url, self.FileImpl):
213 parent_url = parent_url.file_location
214 scm = gclient_scm.CreateSCM(parent_url, self.root_dir(), None)
215 self.parsed_url = scm.FullUrlForRelativeUrl(url)
216 else:
217 self.parsed_url = url
218 logging.debug('%s, %s -> %s' % (self.name, url, self.parsed_url))
219 elif isinstance(url, self.FileImpl):
220 self.parsed_url = url
221 logging.debug('%s, %s -> %s (File)' % (self.name, url, self.parsed_url))
222 return self.parsed_url
223
maruel@chromium.org271375b2010-06-23 19:17:38 +0000224 def ParseDepsFile(self, direct_reference):
225 """Parses the DEPS file for this dependency."""
226 if direct_reference:
227 # Maybe it was referenced earlier by a From() keyword but it's now
228 # directly referenced.
229 self.direct_reference = direct_reference
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000230 if self.deps_parsed:
231 return
maruel@chromium.org271375b2010-06-23 19:17:38 +0000232 self.deps_parsed = True
233 filepath = os.path.join(self.root_dir(), self.name, self.deps_file)
234 if not os.path.isfile(filepath):
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000235 return
maruel@chromium.org271375b2010-06-23 19:17:38 +0000236 deps_content = gclient_utils.FileRead(filepath)
maruel@chromium.org0d425922010-06-21 19:22:24 +0000237
maruel@chromium.org271375b2010-06-23 19:17:38 +0000238 # Eval the content.
239 # One thing is unintuitive, vars= {} must happen before Var() use.
240 local_scope = {}
241 var = self.VarImpl(self.custom_vars, local_scope)
242 global_scope = {
243 'File': self.FileImpl,
244 'From': self.FromImpl,
245 'Var': var.Lookup,
246 'deps_os': {},
247 }
maruel@chromium.org5990f9d2010-07-07 18:02:58 +0000248 try:
249 exec(deps_content, global_scope, local_scope)
250 except SyntaxError, e:
251 gclient_utils.SyntaxErrorToError(filepath, e)
maruel@chromium.org271375b2010-06-23 19:17:38 +0000252 deps = local_scope.get('deps', {})
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000253 # load os specific dependencies if defined. these dependencies may
254 # override or extend the values defined by the 'deps' member.
maruel@chromium.org271375b2010-06-23 19:17:38 +0000255 if 'deps_os' in local_scope:
256 for deps_os_key in self.enforced_os():
257 os_deps = local_scope['deps_os'].get(deps_os_key, {})
258 if len(self.enforced_os()) > 1:
259 # Ignore any conflict when including deps for more than one
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000260 # platform, so we collect the broadest set of dependencies available.
261 # We may end up with the wrong revision of something for our
262 # platform, but this is the best we can do.
263 deps.update([x for x in os_deps.items() if not x[0] in deps])
264 else:
265 deps.update(os_deps)
266
maruel@chromium.org271375b2010-06-23 19:17:38 +0000267 self.deps_hooks.extend(local_scope.get('hooks', []))
268
269 # If a line is in custom_deps, but not in the solution, we want to append
270 # this line to the solution.
271 for d in self.custom_deps:
272 if d not in deps:
273 deps[d] = self.custom_deps[d]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000274
275 # If use_relative_paths is set in the DEPS file, regenerate
276 # the dictionary using paths relative to the directory containing
277 # the DEPS file.
maruel@chromium.org271375b2010-06-23 19:17:38 +0000278 use_relative_paths = local_scope.get('use_relative_paths', False)
279 if use_relative_paths:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000280 rel_deps = {}
281 for d, url in deps.items():
282 # normpath is required to allow DEPS to use .. in their
283 # dependency local path.
maruel@chromium.org271375b2010-06-23 19:17:38 +0000284 rel_deps[os.path.normpath(os.path.join(self.name, d))] = url
285 deps = rel_deps
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000286
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000287 # Convert the deps into real Dependency.
288 for name, url in deps.iteritems():
289 if name in [s.name for s in self.dependencies]:
290 raise
291 self.dependencies.append(Dependency(self, name, url))
292 # Sort by name.
293 self.dependencies.sort(key=lambda x: x.name)
294 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.org9a66ddf2010-06-16 16:54:16 +0000441
442class GClient(Dependency):
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000443 """Object that represent a gclient checkout. A tree of Dependency(), one per
444 solution or DEPS entry."""
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000445
446 DEPS_OS_CHOICES = {
447 "win32": "win",
448 "win": "win",
449 "cygwin": "win",
450 "darwin": "mac",
451 "mac": "mac",
452 "unix": "unix",
453 "linux": "unix",
454 "linux2": "unix",
455 }
456
457 DEFAULT_CLIENT_FILE_TEXT = ("""\
458solutions = [
459 { "name" : "%(solution_name)s",
460 "url" : "%(solution_url)s",
461 "custom_deps" : {
462 },
maruel@chromium.org73e21142010-07-05 13:32:01 +0000463 "safesync_url": "%(safesync_url)s",
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000464 },
465]
466""")
467
468 DEFAULT_SNAPSHOT_SOLUTION_TEXT = ("""\
469 { "name" : "%(solution_name)s",
470 "url" : "%(solution_url)s",
471 "custom_deps" : {
maruel@chromium.org73e21142010-07-05 13:32:01 +0000472%(solution_deps)s },
473 "safesync_url": "%(safesync_url)s",
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000474 },
475""")
476
477 DEFAULT_SNAPSHOT_FILE_TEXT = ("""\
478# Snapshot generated with gclient revinfo --snapshot
479solutions = [
maruel@chromium.org73e21142010-07-05 13:32:01 +0000480%(solution_list)s]
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000481""")
482
483 def __init__(self, root_dir, options):
484 Dependency.__init__(self, None, None, None)
maruel@chromium.org0d425922010-06-21 19:22:24 +0000485 self._options = options
maruel@chromium.org271375b2010-06-23 19:17:38 +0000486 if options.deps_os:
487 enforced_os = options.deps_os.split(',')
488 else:
489 enforced_os = [self.DEPS_OS_CHOICES.get(sys.platform, 'unix')]
490 if 'all' in enforced_os:
491 enforced_os = self.DEPS_OS_CHOICES.itervalues()
492 self._enforced_os = list(set(enforced_os))
493 self._root_dir = root_dir
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000494 self.config_content = None
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000495 # Do not change previous behavior. Only solution level and immediate DEPS
496 # are processed.
497 self._recursion_limit = 2
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000498
499 def SetConfig(self, content):
500 assert self.dependencies == []
501 config_dict = {}
502 self.config_content = content
503 try:
504 exec(content, config_dict)
505 except SyntaxError, e:
maruel@chromium.org5990f9d2010-07-07 18:02:58 +0000506 gclient_utils.SyntaxErrorToError('.gclient', e)
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000507 for s in config_dict.get('solutions', []):
maruel@chromium.org81843b82010-06-28 16:49:26 +0000508 try:
509 self.dependencies.append(Dependency(
510 self, s['name'], s['url'],
511 s.get('safesync_url', None),
512 s.get('custom_deps', {}),
513 s.get('custom_vars', {})))
514 except KeyError:
515 raise gclient_utils.Error('Invalid .gclient file. Solution is '
516 'incomplete: %s' % s)
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000517 # .gclient can have hooks.
518 self.deps_hooks = config_dict.get('hooks', [])
519
520 def SaveConfig(self):
521 gclient_utils.FileWrite(os.path.join(self.root_dir(),
522 self._options.config_filename),
523 self.config_content)
524
525 @staticmethod
526 def LoadCurrentConfig(options):
527 """Searches for and loads a .gclient file relative to the current working
528 dir. Returns a GClient object."""
529 path = gclient_utils.FindGclientRoot(os.getcwd(), options.config_filename)
530 if not path:
531 return None
532 client = GClient(path, options)
533 client.SetConfig(gclient_utils.FileRead(
534 os.path.join(path, options.config_filename)))
535 return client
536
537 def SetDefaultConfig(self, solution_name, solution_url, safesync_url):
538 self.SetConfig(self.DEFAULT_CLIENT_FILE_TEXT % {
539 'solution_name': solution_name,
540 'solution_url': solution_url,
541 'safesync_url' : safesync_url,
542 })
543
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000544 def _SaveEntries(self):
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000545 """Creates a .gclient_entries file to record the list of unique checkouts.
546
547 The .gclient_entries file lives in the same directory as .gclient.
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000548 """
549 # Sometimes pprint.pformat will use {', sometimes it'll use { ' ... It
550 # makes testing a bit too fun.
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000551 result = 'entries = {\n'
552 for entry in self.tree(False):
553 # Skip over File() dependencies as we can't version them.
554 if not isinstance(entry.parsed_url, self.FileImpl):
555 result += ' %s: %s,\n' % (pprint.pformat(entry.name),
556 pprint.pformat(entry.parsed_url))
557 result += '}\n'
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000558 file_path = os.path.join(self.root_dir(), self._options.entries_filename)
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000559 logging.info(result)
560 gclient_utils.FileWrite(file_path, result)
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000561
562 def _ReadEntries(self):
563 """Read the .gclient_entries file for the given client.
564
565 Returns:
566 A sequence of solution names, which will be empty if there is the
567 entries file hasn't been created yet.
568 """
569 scope = {}
570 filename = os.path.join(self.root_dir(), self._options.entries_filename)
571 if not os.path.exists(filename):
maruel@chromium.org73e21142010-07-05 13:32:01 +0000572 return {}
maruel@chromium.org5990f9d2010-07-07 18:02:58 +0000573 try:
574 exec(gclient_utils.FileRead(filename), scope)
575 except SyntaxError, e:
576 gclient_utils.SyntaxErrorToError(filename, e)
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000577 return scope['entries']
578
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000579 def _EnforceRevisions(self):
maruel@chromium.org918a9ae2010-05-28 15:50:30 +0000580 """Checks for revision overrides."""
581 revision_overrides = {}
maruel@chromium.org307d1792010-05-31 20:03:13 +0000582 if self._options.head:
583 return revision_overrides
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000584 for s in self.dependencies:
585 if not s.safesync_url:
maruel@chromium.org307d1792010-05-31 20:03:13 +0000586 continue
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000587 handle = urllib.urlopen(s.safesync_url)
maruel@chromium.org307d1792010-05-31 20:03:13 +0000588 rev = handle.read().strip()
589 handle.close()
590 if len(rev):
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000591 self._options.revisions.append('%s@%s' % (s.name, rev))
maruel@chromium.org307d1792010-05-31 20:03:13 +0000592 if not self._options.revisions:
593 return revision_overrides
594 # --revision will take over safesync_url.
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000595 solutions_names = [s.name for s in self.dependencies]
maruel@chromium.org307d1792010-05-31 20:03:13 +0000596 index = 0
597 for revision in self._options.revisions:
598 if not '@' in revision:
599 # Support for --revision 123
600 revision = '%s@%s' % (solutions_names[index], revision)
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000601 sol, rev = revision.split('@', 1)
maruel@chromium.org307d1792010-05-31 20:03:13 +0000602 if not sol in solutions_names:
603 #raise gclient_utils.Error('%s is not a valid solution.' % sol)
604 print >> sys.stderr, ('Please fix your script, having invalid '
605 '--revision flags will soon considered an error.')
606 else:
maruel@chromium.org918a9ae2010-05-28 15:50:30 +0000607 revision_overrides[sol] = rev
maruel@chromium.org307d1792010-05-31 20:03:13 +0000608 index += 1
maruel@chromium.org918a9ae2010-05-28 15:50:30 +0000609 return revision_overrides
610
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000611 def RunOnDeps(self, command, args):
612 """Runs a command on each dependency in a client and its dependencies.
613
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000614 Args:
615 command: The command to use (e.g., 'status' or 'diff')
616 args: list of str - extra arguments to add to the command line.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000617 """
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000618 if not self.dependencies:
maruel@chromium.org73e21142010-07-05 13:32:01 +0000619 raise gclient_utils.Error('No solution specified')
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000620 revision_overrides = self._EnforceRevisions()
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000621 pm = None
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000622 if command == 'update' and not self._options.verbose:
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000623 pm = Progress('Syncing projects', len(self.tree(False)) + 1)
624 self.RunCommandRecursively(self._options, revision_overrides,
625 command, args, pm)
626 if pm:
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000627 pm.end()
piman@chromium.org6f363722010-04-27 00:41:09 +0000628
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000629 # Once all the dependencies have been processed, it's now safe to run the
630 # hooks.
631 if not self._options.nohooks:
632 self.RunHooksRecursively(self._options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000633
634 if command == 'update':
ajwong@chromium.orgcdcee802009-06-23 15:30:42 +0000635 # Notify the user if there is an orphaned entry in their working copy.
636 # Only delete the directory if there are no changes in it, and
637 # delete_unversioned_trees is set to true.
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000638 entries = [i.name for i in self.tree(False)]
639 for entry, prev_url in self._ReadEntries().iteritems():
maruel@chromium.orgc5e9aec2009-08-03 18:25:56 +0000640 # Fix path separator on Windows.
641 entry_fixed = entry.replace('/', os.path.sep)
maruel@chromium.org75a59272010-06-11 22:34:03 +0000642 e_dir = os.path.join(self.root_dir(), entry_fixed)
maruel@chromium.orgc5e9aec2009-08-03 18:25:56 +0000643 # Use entry and not entry_fixed there.
maruel@chromium.org0329e672009-05-13 18:41:04 +0000644 if entry not in entries and os.path.exists(e_dir):
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000645 file_list = []
646 scm = gclient_scm.CreateSCM(prev_url, self.root_dir(), entry_fixed)
647 scm.status(self._options, [], file_list)
648 modified_files = file_list != []
msb@chromium.org83017012009-09-28 18:52:12 +0000649 if not self._options.delete_unversioned_trees or modified_files:
maruel@chromium.orgc5e9aec2009-08-03 18:25:56 +0000650 # There are modified files in this entry. Keep warning until
651 # removed.
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000652 print(('\nWARNING: \'%s\' is no longer part of this client. '
653 'It is recommended that you manually remove it.\n') %
maruel@chromium.orgc5e9aec2009-08-03 18:25:56 +0000654 entry_fixed)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000655 else:
656 # Delete the entry
maruel@chromium.org73e21142010-07-05 13:32:01 +0000657 print('\n________ deleting \'%s\' in \'%s\'' % (
658 entry_fixed, self.root_dir()))
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000659 gclient_utils.RemoveDirectory(e_dir)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000660 # record the current list of entries for next time
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000661 self._SaveEntries()
maruel@chromium.org17cdf762010-05-28 17:30:52 +0000662 return 0
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000663
664 def PrintRevInfo(self):
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000665 """Output revision info mapping for the client and its dependencies.
666
667 This allows the capture of an overall "revision" for the source tree that
maruel@chromium.org918a9ae2010-05-28 15:50:30 +0000668 can be used to reproduce the same tree in the future. It is only useful for
669 "unpinned dependencies", i.e. DEPS/deps references without a svn revision
670 number or a git hash. A git branch name isn't "pinned" since the actual
671 commit can change.
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000672
673 The --snapshot option allows creating a .gclient file to reproduce the tree.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000674 """
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000675 if not self.dependencies:
maruel@chromium.org73e21142010-07-05 13:32:01 +0000676 raise gclient_utils.Error('No solution specified')
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000677 # Load all the settings.
678 self.RunCommandRecursively(self._options, {}, None, [], None)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000679
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000680 def GetURLAndRev(name, original_url):
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000681 """Returns the revision-qualified SCM url."""
682 if original_url is None:
maruel@chromium.orgbaa578e2010-07-12 17:36:59 +0000683 return None
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000684 if isinstance(original_url, self.FileImpl):
685 original_url = original_url.file_location
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000686 url, _ = gclient_utils.SplitUrlRevision(original_url)
maruel@chromium.org75a59272010-06-11 22:34:03 +0000687 scm = gclient_scm.CreateSCM(original_url, self.root_dir(), name)
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000688 if not os.path.isdir(scm.checkout_path):
689 return None
maruel@chromium.orgbaa578e2010-07-12 17:36:59 +0000690 return '%s@%s' % (url, scm.revinfo(self._options, [], None))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000691
maruel@chromium.orgbaa578e2010-07-12 17:36:59 +0000692 if self._options.snapshot:
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000693 new_gclient = ''
694 # First level at .gclient
695 for d in self.dependencies:
696 entries = {}
697 def GrabDeps(sol):
698 """Recursively grab dependencies."""
699 for i in sol.dependencies:
700 entries[i.name] = GetURLAndRev(i.name, i.parsed_url)
701 GrabDeps(i)
702 GrabDeps(d)
703 custom_deps = []
704 for k in sorted(entries.keys()):
705 if entries[k]:
706 # Quotes aren't escaped...
707 custom_deps.append(' \"%s\": \'%s\',\n' % (k, entries[k]))
708 else:
709 custom_deps.append(' \"%s\": None,\n' % k)
710 new_gclient += self.DEFAULT_SNAPSHOT_SOLUTION_TEXT % {
711 'solution_name': d.name,
712 'solution_url': d.url,
713 'safesync_url' : d.safesync_url or '',
714 'solution_deps': ''.join(custom_deps),
715 }
716 # Print the snapshot configuration file
717 print(self.DEFAULT_SNAPSHOT_FILE_TEXT % {'solution_list': new_gclient})
nasser@codeaurora.orgde8f3522010-03-11 23:47:44 +0000718 else:
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000719 entries = sorted(self.tree(False), key=lambda i: i.name)
720 for entry in entries:
721 entry_url = GetURLAndRev(entry.name, entry.parsed_url)
722 line = '%s: %s' % (entry.name, entry_url)
723 if not entry is entries[-1]:
724 line += ';'
725 print line
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000726
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000727 def ParseDepsFile(self, direct_reference):
728 """No DEPS to parse for a .gclient file."""
729 self.direct_reference = direct_reference
730 self.deps_parsed = True
731
maruel@chromium.org75a59272010-06-11 22:34:03 +0000732 def root_dir(self):
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000733 """Root directory of gclient checkout."""
maruel@chromium.org75a59272010-06-11 22:34:03 +0000734 return self._root_dir
735
maruel@chromium.org271375b2010-06-23 19:17:38 +0000736 def enforced_os(self):
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000737 """What deps_os entries that are to be parsed."""
maruel@chromium.org271375b2010-06-23 19:17:38 +0000738 return self._enforced_os
739
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000740 def recursion_limit(self):
741 """How recursive can each dependencies in DEPS file can load DEPS file."""
742 return self._recursion_limit
743
744 def tree(self, force_all):
745 """Returns a flat list of all the dependencies."""
746 def subtree(dep):
747 if not force_all and not dep.direct_reference:
748 # Was loaded from a From() keyword in a DEPS file, don't load all its
749 # dependencies.
750 return []
751 result = dep.dependencies[:]
752 for d in dep.dependencies:
753 result.extend(subtree(d))
754 return result
755 return subtree(self)
756
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000757
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000758#### gclient commands.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000759
760
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000761def CMDcleanup(parser, args):
762 """Cleans up all working copies.
maruel@chromium.orgddff62d2010-05-17 21:02:36 +0000763
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000764Mostly svn-specific. Simply runs 'svn cleanup' for each module.
maruel@chromium.org79692d62010-05-14 18:57:13 +0000765"""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000766 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
767 help='override deps for the specified (comma-separated) '
768 'platform(s); \'all\' will process all deps_os '
769 'references')
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000770 (options, args) = parser.parse_args(args)
maruel@chromium.org2806acc2009-05-15 12:33:34 +0000771 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000772 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000773 raise gclient_utils.Error('client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000774 if options.verbose:
775 # Print out the .gclient file. This is longer than if we just printed the
776 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.org116704f2010-06-11 17:34:38 +0000777 print(client.config_content)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000778 return client.RunOnDeps('cleanup', args)
779
780
piman@chromium.org4b90e3a2010-07-01 20:28:26 +0000781@attr('usage', '[command] [args ...]')
782def CMDrecurse(parser, args):
783 """Operates on all the entries.
784
785 Runs a shell command on all entries.
786 """
787 # Stop parsing at the first non-arg so that these go through to the command
788 parser.disable_interspersed_args()
789 parser.add_option('-s', '--scm', action='append', default=[],
790 help='choose scm types to operate upon')
791 options, args = parser.parse_args(args)
792 root, entries = gclient_utils.GetGClientRootAndEntries()
793 scm_set = set()
794 for scm in options.scm:
795 scm_set.update(scm.split(','))
796
797 # Pass in the SCM type as an env variable
798 env = os.environ.copy()
799
800 for path, url in entries.iteritems():
801 scm = gclient_scm.GetScmName(url)
802 if scm_set and scm not in scm_set:
803 continue
804 dir = os.path.normpath(os.path.join(root, path))
805 env['GCLIENT_SCM'] = scm
806 env['GCLIENT_URL'] = url
807 subprocess.Popen(args, cwd=dir, env=env).communicate()
808
809
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000810@attr('usage', '[url] [safesync url]')
811def CMDconfig(parser, args):
maruel@chromium.orgddff62d2010-05-17 21:02:36 +0000812 """Create a .gclient file in the current directory.
813
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000814This specifies the configuration for further commands. After update/sync,
maruel@chromium.org79692d62010-05-14 18:57:13 +0000815top-level DEPS files in each module are read to determine dependent
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000816modules to operate on as well. If optional [url] parameter is
maruel@chromium.org79692d62010-05-14 18:57:13 +0000817provided, then configuration is read from a specified Subversion server
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000818URL.
maruel@chromium.org79692d62010-05-14 18:57:13 +0000819"""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000820 parser.add_option('--spec',
821 help='create a gclient file containing the provided '
822 'string. Due to Cygwin/Python brokenness, it '
823 'probably can\'t contain any newlines.')
824 parser.add_option('--name',
825 help='overrides the default name for the solution')
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000826 (options, args) = parser.parse_args(args)
maruel@chromium.org5fc2a332010-05-26 19:37:15 +0000827 if ((options.spec and args) or len(args) > 2 or
828 (not options.spec and not args)):
829 parser.error('Inconsistent arguments. Use either --spec or one or 2 args')
830
maruel@chromium.org0329e672009-05-13 18:41:04 +0000831 if os.path.exists(options.config_filename):
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000832 raise gclient_utils.Error('%s file already exists in the current directory'
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000833 % options.config_filename)
maruel@chromium.org2806acc2009-05-15 12:33:34 +0000834 client = GClient('.', options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000835 if options.spec:
836 client.SetConfig(options.spec)
837 else:
maruel@chromium.org1ab7ffc2009-06-03 17:21:37 +0000838 base_url = args[0].rstrip('/')
iposva@chromium.org8cf7a392010-04-07 17:20:26 +0000839 if not options.name:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000840 name = base_url.split('/')[-1]
iposva@chromium.org8cf7a392010-04-07 17:20:26 +0000841 else:
842 # specify an alternate relpath for the given URL.
843 name = options.name
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000844 safesync_url = ''
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000845 if len(args) > 1:
846 safesync_url = args[1]
847 client.SetDefaultConfig(name, base_url, safesync_url)
848 client.SaveConfig()
maruel@chromium.org79692d62010-05-14 18:57:13 +0000849 return 0
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000850
851
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000852def CMDexport(parser, args):
maruel@chromium.orgddff62d2010-05-17 21:02:36 +0000853 """Wrapper for svn export for all managed directories."""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000854 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
855 help='override deps for the specified (comma-separated) '
856 'platform(s); \'all\' will process all deps_os '
857 'references')
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000858 (options, args) = parser.parse_args(args)
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +0000859 if len(args) != 1:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000860 raise gclient_utils.Error('Need directory name')
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +0000861 client = GClient.LoadCurrentConfig(options)
862
863 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000864 raise gclient_utils.Error('client not configured; see \'gclient config\'')
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +0000865
866 if options.verbose:
867 # Print out the .gclient file. This is longer than if we just printed the
868 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.org116704f2010-06-11 17:34:38 +0000869 print(client.config_content)
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +0000870 return client.RunOnDeps('export', args)
871
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000872
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000873@attr('epilog', """Example:
874 gclient pack > patch.txt
875 generate simple patch for configured client and dependences
876""")
877def CMDpack(parser, args):
maruel@chromium.org79692d62010-05-14 18:57:13 +0000878 """Generate a patch which can be applied at the root of the tree.
maruel@chromium.orgddff62d2010-05-17 21:02:36 +0000879
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000880Internally, runs 'svn diff'/'git diff' on each checked out module and
maruel@chromium.org79692d62010-05-14 18:57:13 +0000881dependencies, and performs minimal postprocessing of the output. The
882resulting patch is printed to stdout and can be applied to a freshly
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000883checked out tree via 'patch -p0 < patchfile'.
maruel@chromium.org79692d62010-05-14 18:57:13 +0000884"""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000885 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
886 help='override deps for the specified (comma-separated) '
887 'platform(s); \'all\' will process all deps_os '
888 'references')
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000889 (options, args) = parser.parse_args(args)
kbr@google.comab318592009-09-04 00:54:55 +0000890 client = GClient.LoadCurrentConfig(options)
891 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000892 raise gclient_utils.Error('client not configured; see \'gclient config\'')
kbr@google.comab318592009-09-04 00:54:55 +0000893 if options.verbose:
894 # Print out the .gclient file. This is longer than if we just printed the
895 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.org116704f2010-06-11 17:34:38 +0000896 print(client.config_content)
kbr@google.comab318592009-09-04 00:54:55 +0000897 return client.RunOnDeps('pack', args)
898
899
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000900def CMDstatus(parser, args):
901 """Show modification status for every dependencies."""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000902 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
903 help='override deps for the specified (comma-separated) '
904 'platform(s); \'all\' will process all deps_os '
905 'references')
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000906 (options, args) = parser.parse_args(args)
maruel@chromium.org2806acc2009-05-15 12:33:34 +0000907 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000908 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000909 raise gclient_utils.Error('client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000910 if options.verbose:
911 # Print out the .gclient file. This is longer than if we just printed the
912 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.org116704f2010-06-11 17:34:38 +0000913 print(client.config_content)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000914 return client.RunOnDeps('status', args)
915
916
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000917@attr('epilog', """Examples:
maruel@chromium.org79692d62010-05-14 18:57:13 +0000918 gclient sync
919 update files from SCM according to current configuration,
920 *for modules which have changed since last update or sync*
921 gclient sync --force
922 update files from SCM according to current configuration, for
923 all modules (useful for recovering files deleted from local copy)
924 gclient sync --revision src@31000
925 update src directory to r31000
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000926""")
927def CMDsync(parser, args):
928 """Checkout/update all modules."""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000929 parser.add_option('-f', '--force', action='store_true',
930 help='force update even for unchanged modules')
931 parser.add_option('-n', '--nohooks', action='store_true',
932 help='don\'t run hooks after the update is complete')
933 parser.add_option('-r', '--revision', action='append',
934 dest='revisions', metavar='REV', default=[],
935 help='Enforces revision/hash for the solutions with the '
936 'format src@rev. The src@ part is optional and can be '
937 'skipped. -r can be used multiple times when .gclient '
938 'has multiple solutions configured and will work even '
939 'if the src@ part is skipped.')
940 parser.add_option('-H', '--head', action='store_true',
941 help='skips any safesync_urls specified in '
942 'configured solutions and sync to head instead')
943 parser.add_option('-D', '--delete_unversioned_trees', action='store_true',
944 help='delete any unexpected unversioned trees '
945 'that are in the checkout')
946 parser.add_option('-R', '--reset', action='store_true',
947 help='resets any local changes before updating (git only)')
948 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
949 help='override deps for the specified (comma-separated) '
950 'platform(s); \'all\' will process all deps_os '
951 'references')
952 parser.add_option('-m', '--manually_grab_svn_rev', action='store_true',
953 help='Skip svn up whenever possible by requesting '
954 'actual HEAD revision from the repository')
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000955 (options, args) = parser.parse_args(args)
maruel@chromium.org2806acc2009-05-15 12:33:34 +0000956 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000957
958 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000959 raise gclient_utils.Error('client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000960
maruel@chromium.org307d1792010-05-31 20:03:13 +0000961 if options.revisions and options.head:
962 # TODO(maruel): Make it a parser.error if it doesn't break any builder.
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000963 print('Warning: you cannot use both --head and --revision')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000964
965 if options.verbose:
966 # Print out the .gclient file. This is longer than if we just printed the
967 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.org116704f2010-06-11 17:34:38 +0000968 print(client.config_content)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000969 return client.RunOnDeps('update', args)
970
971
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000972def CMDupdate(parser, args):
maruel@chromium.orgddff62d2010-05-17 21:02:36 +0000973 """Alias for the sync command. Deprecated."""
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000974 return CMDsync(parser, args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000975
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000976def CMDdiff(parser, args):
977 """Displays local diff for every dependencies."""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000978 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
979 help='override deps for the specified (comma-separated) '
980 'platform(s); \'all\' will process all deps_os '
981 'references')
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000982 (options, args) = parser.parse_args(args)
maruel@chromium.org2806acc2009-05-15 12:33:34 +0000983 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000984 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000985 raise gclient_utils.Error('client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000986 if options.verbose:
987 # Print out the .gclient file. This is longer than if we just printed the
988 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.org116704f2010-06-11 17:34:38 +0000989 print(client.config_content)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000990 return client.RunOnDeps('diff', args)
991
992
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000993def CMDrevert(parser, args):
994 """Revert all modifications in every dependencies."""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000995 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
996 help='override deps for the specified (comma-separated) '
997 'platform(s); \'all\' will process all deps_os '
998 'references')
999 parser.add_option('-n', '--nohooks', action='store_true',
1000 help='don\'t run hooks after the revert is complete')
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001001 (options, args) = parser.parse_args(args)
1002 # --force is implied.
1003 options.force = True
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001004 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001005 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00001006 raise gclient_utils.Error('client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001007 return client.RunOnDeps('revert', args)
1008
1009
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001010def CMDrunhooks(parser, args):
1011 """Runs hooks for files that have been modified in the local working copy."""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00001012 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
1013 help='override deps for the specified (comma-separated) '
1014 'platform(s); \'all\' will process all deps_os '
1015 'references')
1016 parser.add_option('-f', '--force', action='store_true', default=True,
1017 help='Deprecated. No effect.')
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001018 (options, args) = parser.parse_args(args)
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001019 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001020 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00001021 raise gclient_utils.Error('client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001022 if options.verbose:
1023 # Print out the .gclient file. This is longer than if we just printed the
1024 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.org116704f2010-06-11 17:34:38 +00001025 print(client.config_content)
maruel@chromium.org5df6a462009-08-28 18:52:26 +00001026 options.force = True
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001027 options.nohooks = False
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001028 return client.RunOnDeps('runhooks', args)
1029
1030
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001031def CMDrevinfo(parser, args):
maruel@chromium.org9eda4112010-06-11 18:56:10 +00001032 """Output revision info mapping for the client and its dependencies.
1033
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00001034 This allows the capture of an overall 'revision' for the source tree that
maruel@chromium.org9eda4112010-06-11 18:56:10 +00001035 can be used to reproduce the same tree in the future. It is only useful for
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00001036 'unpinned dependencies', i.e. DEPS/deps references without a svn revision
1037 number or a git hash. A git branch name isn't 'pinned' since the actual
maruel@chromium.org9eda4112010-06-11 18:56:10 +00001038 commit can change.
1039 """
1040 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
1041 help='override deps for the specified (comma-separated) '
1042 'platform(s); \'all\' will process all deps_os '
1043 'references')
1044 parser.add_option('-s', '--snapshot', action='store_true',
1045 help='creates a snapshot .gclient file of the current '
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +00001046 'version of all repositories to reproduce the tree')
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001047 (options, args) = parser.parse_args(args)
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001048 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001049 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00001050 raise gclient_utils.Error('client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001051 client.PrintRevInfo()
maruel@chromium.org79692d62010-05-14 18:57:13 +00001052 return 0
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001053
1054
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001055def Command(name):
1056 return getattr(sys.modules[__name__], 'CMD' + name, None)
1057
1058
1059def CMDhelp(parser, args):
1060 """Prints list of commands or help for a specific command."""
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001061 (_, args) = parser.parse_args(args)
maruel@chromium.orgddff62d2010-05-17 21:02:36 +00001062 if len(args) == 1:
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001063 return Main(args + ['--help'])
maruel@chromium.orgddff62d2010-05-17 21:02:36 +00001064 parser.print_help()
1065 return 0
1066
1067
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001068def GenUsage(parser, command):
1069 """Modify an OptParse object with the function's documentation."""
1070 obj = Command(command)
1071 if command == 'help':
1072 command = '<command>'
1073 # OptParser.description prefer nicely non-formatted strings.
1074 parser.description = re.sub('[\r\n ]{2,}', ' ', obj.__doc__)
1075 usage = getattr(obj, 'usage', '')
1076 parser.set_usage('%%prog %s [options] %s' % (command, usage))
1077 parser.epilog = getattr(obj, 'epilog', None)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001078
1079
1080def Main(argv):
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001081 """Doesn't parse the arguments here, just find the right subcommand to
1082 execute."""
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001083 try:
1084 # Do it late so all commands are listed.
1085 CMDhelp.usage = ('\n\nCommands are:\n' + '\n'.join([
1086 ' %-10s %s' % (fn[3:], Command(fn[3:]).__doc__.split('\n')[0].strip())
1087 for fn in dir(sys.modules[__name__]) if fn.startswith('CMD')]))
1088 parser = optparse.OptionParser(version='%prog ' + __version__)
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001089 parser.add_option('-v', '--verbose', action='count', default=0,
1090 help='Produces additional output for diagnostics. Can be '
1091 'used up to three times for more logging info.')
1092 parser.add_option('--gclientfile', dest='config_filename',
1093 default=os.environ.get('GCLIENT_FILE', '.gclient'),
1094 help='Specify an alternate %default file')
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001095 # Integrate standard options processing.
1096 old_parser = parser.parse_args
1097 def Parse(args):
1098 (options, args) = old_parser(args)
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001099 level = None
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001100 if options.verbose == 2:
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001101 level = logging.INFO
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001102 elif options.verbose > 2:
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001103 level = logging.DEBUG
1104 logging.basicConfig(level=level,
1105 format='%(module)s(%(lineno)d) %(funcName)s:%(message)s')
1106 options.entries_filename = options.config_filename + '_entries'
maruel@chromium.orge3216c62010-07-08 03:31:43 +00001107
1108 # These hacks need to die.
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001109 if not hasattr(options, 'revisions'):
1110 # GClient.RunOnDeps expects it even if not applicable.
1111 options.revisions = []
1112 if not hasattr(options, 'head'):
1113 options.head = None
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001114 if not hasattr(options, 'nohooks'):
1115 options.nohooks = True
1116 if not hasattr(options, 'deps_os'):
1117 options.deps_os = None
maruel@chromium.orge3216c62010-07-08 03:31:43 +00001118 if not hasattr(options, 'manually_grab_svn_rev'):
1119 options.manually_grab_svn_rev = None
1120 if not hasattr(options, 'force'):
1121 options.force = None
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001122 return (options, args)
1123 parser.parse_args = Parse
1124 # We don't want wordwrapping in epilog (usually examples)
1125 parser.format_epilog = lambda _: parser.epilog or ''
1126 if argv:
1127 command = Command(argv[0])
1128 if command:
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001129 # 'fix' the usage and the description now that we know the subcommand.
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001130 GenUsage(parser, argv[0])
1131 return command(parser, argv[1:])
1132 # Not a known command. Default to help.
1133 GenUsage(parser, 'help')
1134 return CMDhelp(parser, argv)
1135 except gclient_utils.Error, e:
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001136 print >> sys.stderr, 'Error: %s' % str(e)
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001137 return 1
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001138
1139
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001140if '__main__' == __name__:
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001141 sys.exit(Main(sys.argv[1:]))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001142
1143# vim: ts=2:sw=2:tw=80:et: