blob: c3e696423783fb632eae6a6ba9564e302e495ff3 [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.
maruel@chromium.org861fd0f2010-07-23 03:05:05 +0000156 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]
maruel@chromium.org98d05fa2010-07-22 21:58:01 +0000194 sub_target = url.sub_target_name or self.name
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000195 # 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))
bradnelson@google.com5f8f2a82010-07-23 00:05:46 +0000299 # Sort by name.
300 self.dependencies.sort(key=lambda x: x.name)
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."""
maruel@chromium.org861fd0f2010-07-23 03:05:05 +0000307 assert self._file_list == []
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000308 # 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()],
maruel@chromium.org861fd0f2010-07-23 03:05:05 +0000323 self._file_list)
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000324 else:
325 options.revision = revision_overrides.get(self.name)
326 scm = gclient_scm.CreateSCM(self.parsed_url, self.root_dir(), self.name)
maruel@chromium.org861fd0f2010-07-23 03:05:05 +0000327 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]
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000330 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.
maruel@chromium.org861fd0f2010-07-23 03:05:05 +0000368 file_list = self.file_list()
369 for i in range(len(file_list)):
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000370 # It depends on the command being executed (like runhooks vs sync).
maruel@chromium.org861fd0f2010-07-23 03:05:05 +0000371 if not os.path.isabs(file_list[i]):
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000372 continue
maruel@chromium.orgdc7445d2010-07-09 21:05:29 +0000373
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000374 prefix = os.path.commonprefix([self.root_dir().lower(),
maruel@chromium.org861fd0f2010-07-23 03:05:05 +0000375 file_list[i].lower()])
376 file_list[i] = file_list[i][len(prefix):]
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000377
378 # Strip any leading path separators.
maruel@chromium.org861fd0f2010-07-23 03:05:05 +0000379 while (file_list[i].startswith('\\') or
380 file_list[i].startswith('/')):
381 file_list[i] = file_list[i][1:]
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000382
383 # Run hooks on the basis of whether the files from the gclient operation
384 # match each hook's pattern.
385 for hook_dict in self.deps_hooks:
386 pattern = re.compile(hook_dict['pattern'])
maruel@chromium.org861fd0f2010-07-23 03:05:05 +0000387 matching_file_list = [f for f in file_list if pattern.search(f)]
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000388 if matching_file_list:
389 self._RunHookAction(hook_dict, matching_file_list)
390 if self.recursion_limit():
391 for s in self.dependencies:
392 s.RunHooksRecursively(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000393
maruel@chromium.orgeaf61062010-07-07 18:42:39 +0000394 def _RunHookAction(self, hook_dict, matching_file_list):
395 """Runs the action from a single hook."""
396 logging.info(hook_dict)
397 logging.info(matching_file_list)
398 command = hook_dict['action'][:]
399 if command[0] == 'python':
400 # If the hook specified "python" as the first item, the action is a
401 # Python script. Run it by starting a new copy of the same
402 # interpreter.
403 command[0] = sys.executable
404
405 if '$matching_files' in command:
406 splice_index = command.index('$matching_files')
407 command[splice_index:splice_index + 1] = matching_file_list
408
409 # Use a discrete exit status code of 2 to indicate that a hook action
410 # failed. Users of this script may wish to treat hook action failures
411 # differently from VC failures.
412 return gclient_utils.SubprocessCall(command, self.root_dir(), fail_status=2)
413
maruel@chromium.org271375b2010-06-23 19:17:38 +0000414 def root_dir(self):
415 return self.parent.root_dir()
416
417 def enforced_os(self):
418 return self.parent.enforced_os()
419
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000420 def recursion_limit(self):
421 return self.parent.recursion_limit() - 1
422
423 def tree(self, force_all):
424 return self.parent.tree(force_all)
425
maruel@chromium.orgc57e4f22010-07-22 21:37:46 +0000426 def subtree(self, force_all):
427 result = []
428 # Add breadth-first.
429 if self.direct_reference or force_all:
430 for d in self.dependencies:
maruel@chromium.org044f4e32010-07-22 21:59:57 +0000431 result.append(d)
maruel@chromium.orgc57e4f22010-07-22 21:37:46 +0000432 for d in self.dependencies:
433 result.extend(d.subtree(force_all))
434 return result
435
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000436 def get_custom_deps(self, name, url):
437 """Returns a custom deps if applicable."""
438 if self.parent:
439 url = self.parent.get_custom_deps(name, url)
440 # None is a valid return value to disable a dependency.
441 return self.custom_deps.get(name, url)
442
maruel@chromium.org861fd0f2010-07-23 03:05:05 +0000443 def file_list(self):
444 result = self._file_list[:]
445 for d in self.dependencies:
446 result.extend(d.file_list())
447 return result
448
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000449 def __str__(self):
450 out = []
451 for i in ('name', 'url', 'safesync_url', 'custom_deps', 'custom_vars',
maruel@chromium.org861fd0f2010-07-23 03:05:05 +0000452 'deps_hooks', '_file_list'):
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000453 # 'deps_file'
454 if self.__dict__[i]:
455 out.append('%s: %s' % (i, self.__dict__[i]))
456
457 for d in self.dependencies:
458 out.extend([' ' + x for x in str(d).splitlines()])
459 out.append('')
460 return '\n'.join(out)
461
462 def __repr__(self):
463 return '%s: %s' % (self.name, self.url)
464
maruel@chromium.orgbffb9042010-07-22 20:59:36 +0000465 def hierarchy(self):
maruel@chromium.orgbc2d2f92010-07-22 21:26:48 +0000466 """Returns a human-readable hierarchical reference to a Dependency."""
maruel@chromium.orgbffb9042010-07-22 20:59:36 +0000467 out = '%s(%s)' % (self.name, self.url)
468 i = self.parent
469 while i and i.name:
470 out = '%s(%s) -> %s' % (i.name, i.url, out)
471 i = i.parent
472 return out
473
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000474
475class GClient(Dependency):
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000476 """Object that represent a gclient checkout. A tree of Dependency(), one per
477 solution or DEPS entry."""
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000478
479 DEPS_OS_CHOICES = {
480 "win32": "win",
481 "win": "win",
482 "cygwin": "win",
483 "darwin": "mac",
484 "mac": "mac",
485 "unix": "unix",
486 "linux": "unix",
487 "linux2": "unix",
488 }
489
490 DEFAULT_CLIENT_FILE_TEXT = ("""\
491solutions = [
492 { "name" : "%(solution_name)s",
493 "url" : "%(solution_url)s",
494 "custom_deps" : {
495 },
maruel@chromium.org73e21142010-07-05 13:32:01 +0000496 "safesync_url": "%(safesync_url)s",
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000497 },
498]
499""")
500
501 DEFAULT_SNAPSHOT_SOLUTION_TEXT = ("""\
502 { "name" : "%(solution_name)s",
503 "url" : "%(solution_url)s",
504 "custom_deps" : {
maruel@chromium.org73e21142010-07-05 13:32:01 +0000505%(solution_deps)s },
506 "safesync_url": "%(safesync_url)s",
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000507 },
508""")
509
510 DEFAULT_SNAPSHOT_FILE_TEXT = ("""\
511# Snapshot generated with gclient revinfo --snapshot
512solutions = [
maruel@chromium.org73e21142010-07-05 13:32:01 +0000513%(solution_list)s]
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000514""")
515
516 def __init__(self, root_dir, options):
517 Dependency.__init__(self, None, None, None)
maruel@chromium.org0d425922010-06-21 19:22:24 +0000518 self._options = options
maruel@chromium.org271375b2010-06-23 19:17:38 +0000519 if options.deps_os:
520 enforced_os = options.deps_os.split(',')
521 else:
522 enforced_os = [self.DEPS_OS_CHOICES.get(sys.platform, 'unix')]
523 if 'all' in enforced_os:
524 enforced_os = self.DEPS_OS_CHOICES.itervalues()
525 self._enforced_os = list(set(enforced_os))
526 self._root_dir = root_dir
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000527 self.config_content = None
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000528 # Do not change previous behavior. Only solution level and immediate DEPS
529 # are processed.
530 self._recursion_limit = 2
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000531
532 def SetConfig(self, content):
533 assert self.dependencies == []
534 config_dict = {}
535 self.config_content = content
536 try:
537 exec(content, config_dict)
538 except SyntaxError, e:
maruel@chromium.org5990f9d2010-07-07 18:02:58 +0000539 gclient_utils.SyntaxErrorToError('.gclient', e)
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000540 for s in config_dict.get('solutions', []):
maruel@chromium.org81843b82010-06-28 16:49:26 +0000541 try:
542 self.dependencies.append(Dependency(
543 self, s['name'], s['url'],
544 s.get('safesync_url', None),
545 s.get('custom_deps', {}),
546 s.get('custom_vars', {})))
547 except KeyError:
548 raise gclient_utils.Error('Invalid .gclient file. Solution is '
549 'incomplete: %s' % s)
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000550 # .gclient can have hooks.
551 self.deps_hooks = config_dict.get('hooks', [])
552
553 def SaveConfig(self):
554 gclient_utils.FileWrite(os.path.join(self.root_dir(),
555 self._options.config_filename),
556 self.config_content)
557
558 @staticmethod
559 def LoadCurrentConfig(options):
560 """Searches for and loads a .gclient file relative to the current working
561 dir. Returns a GClient object."""
562 path = gclient_utils.FindGclientRoot(os.getcwd(), options.config_filename)
563 if not path:
564 return None
565 client = GClient(path, options)
566 client.SetConfig(gclient_utils.FileRead(
567 os.path.join(path, options.config_filename)))
568 return client
569
570 def SetDefaultConfig(self, solution_name, solution_url, safesync_url):
571 self.SetConfig(self.DEFAULT_CLIENT_FILE_TEXT % {
572 'solution_name': solution_name,
573 'solution_url': solution_url,
574 'safesync_url' : safesync_url,
575 })
576
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000577 def _SaveEntries(self):
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000578 """Creates a .gclient_entries file to record the list of unique checkouts.
579
580 The .gclient_entries file lives in the same directory as .gclient.
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000581 """
582 # Sometimes pprint.pformat will use {', sometimes it'll use { ' ... It
583 # makes testing a bit too fun.
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000584 result = 'entries = {\n'
585 for entry in self.tree(False):
586 # Skip over File() dependencies as we can't version them.
587 if not isinstance(entry.parsed_url, self.FileImpl):
588 result += ' %s: %s,\n' % (pprint.pformat(entry.name),
589 pprint.pformat(entry.parsed_url))
590 result += '}\n'
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000591 file_path = os.path.join(self.root_dir(), self._options.entries_filename)
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000592 logging.info(result)
593 gclient_utils.FileWrite(file_path, result)
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000594
595 def _ReadEntries(self):
596 """Read the .gclient_entries file for the given client.
597
598 Returns:
599 A sequence of solution names, which will be empty if there is the
600 entries file hasn't been created yet.
601 """
602 scope = {}
603 filename = os.path.join(self.root_dir(), self._options.entries_filename)
604 if not os.path.exists(filename):
maruel@chromium.org73e21142010-07-05 13:32:01 +0000605 return {}
maruel@chromium.org5990f9d2010-07-07 18:02:58 +0000606 try:
607 exec(gclient_utils.FileRead(filename), scope)
608 except SyntaxError, e:
609 gclient_utils.SyntaxErrorToError(filename, e)
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000610 return scope['entries']
611
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000612 def _EnforceRevisions(self):
maruel@chromium.org918a9ae2010-05-28 15:50:30 +0000613 """Checks for revision overrides."""
614 revision_overrides = {}
maruel@chromium.org307d1792010-05-31 20:03:13 +0000615 if self._options.head:
616 return revision_overrides
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000617 for s in self.dependencies:
618 if not s.safesync_url:
maruel@chromium.org307d1792010-05-31 20:03:13 +0000619 continue
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000620 handle = urllib.urlopen(s.safesync_url)
maruel@chromium.org307d1792010-05-31 20:03:13 +0000621 rev = handle.read().strip()
622 handle.close()
623 if len(rev):
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000624 self._options.revisions.append('%s@%s' % (s.name, rev))
maruel@chromium.org307d1792010-05-31 20:03:13 +0000625 if not self._options.revisions:
626 return revision_overrides
627 # --revision will take over safesync_url.
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000628 solutions_names = [s.name for s in self.dependencies]
maruel@chromium.org307d1792010-05-31 20:03:13 +0000629 index = 0
630 for revision in self._options.revisions:
631 if not '@' in revision:
632 # Support for --revision 123
633 revision = '%s@%s' % (solutions_names[index], revision)
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000634 sol, rev = revision.split('@', 1)
maruel@chromium.org307d1792010-05-31 20:03:13 +0000635 if not sol in solutions_names:
636 #raise gclient_utils.Error('%s is not a valid solution.' % sol)
637 print >> sys.stderr, ('Please fix your script, having invalid '
638 '--revision flags will soon considered an error.')
639 else:
maruel@chromium.org918a9ae2010-05-28 15:50:30 +0000640 revision_overrides[sol] = rev
maruel@chromium.org307d1792010-05-31 20:03:13 +0000641 index += 1
maruel@chromium.org918a9ae2010-05-28 15:50:30 +0000642 return revision_overrides
643
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000644 def RunOnDeps(self, command, args):
645 """Runs a command on each dependency in a client and its dependencies.
646
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000647 Args:
648 command: The command to use (e.g., 'status' or 'diff')
649 args: list of str - extra arguments to add to the command line.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000650 """
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000651 if not self.dependencies:
maruel@chromium.org73e21142010-07-05 13:32:01 +0000652 raise gclient_utils.Error('No solution specified')
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000653 revision_overrides = self._EnforceRevisions()
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000654 pm = None
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000655 if command == 'update' and not self._options.verbose:
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000656 pm = Progress('Syncing projects', len(self.tree(False)) + 1)
657 self.RunCommandRecursively(self._options, revision_overrides,
658 command, args, pm)
659 if pm:
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000660 pm.end()
piman@chromium.org6f363722010-04-27 00:41:09 +0000661
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000662 # Once all the dependencies have been processed, it's now safe to run the
663 # hooks.
664 if not self._options.nohooks:
665 self.RunHooksRecursively(self._options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000666
667 if command == 'update':
ajwong@chromium.orgcdcee802009-06-23 15:30:42 +0000668 # Notify the user if there is an orphaned entry in their working copy.
669 # Only delete the directory if there are no changes in it, and
670 # delete_unversioned_trees is set to true.
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000671 entries = [i.name for i in self.tree(False)]
672 for entry, prev_url in self._ReadEntries().iteritems():
maruel@chromium.orgc5e9aec2009-08-03 18:25:56 +0000673 # Fix path separator on Windows.
674 entry_fixed = entry.replace('/', os.path.sep)
maruel@chromium.org75a59272010-06-11 22:34:03 +0000675 e_dir = os.path.join(self.root_dir(), entry_fixed)
maruel@chromium.orgc5e9aec2009-08-03 18:25:56 +0000676 # Use entry and not entry_fixed there.
maruel@chromium.org0329e672009-05-13 18:41:04 +0000677 if entry not in entries and os.path.exists(e_dir):
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000678 file_list = []
679 scm = gclient_scm.CreateSCM(prev_url, self.root_dir(), entry_fixed)
680 scm.status(self._options, [], file_list)
681 modified_files = file_list != []
msb@chromium.org83017012009-09-28 18:52:12 +0000682 if not self._options.delete_unversioned_trees or modified_files:
maruel@chromium.orgc5e9aec2009-08-03 18:25:56 +0000683 # There are modified files in this entry. Keep warning until
684 # removed.
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000685 print(('\nWARNING: \'%s\' is no longer part of this client. '
686 'It is recommended that you manually remove it.\n') %
maruel@chromium.orgc5e9aec2009-08-03 18:25:56 +0000687 entry_fixed)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000688 else:
689 # Delete the entry
maruel@chromium.org73e21142010-07-05 13:32:01 +0000690 print('\n________ deleting \'%s\' in \'%s\'' % (
691 entry_fixed, self.root_dir()))
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000692 gclient_utils.RemoveDirectory(e_dir)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000693 # record the current list of entries for next time
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000694 self._SaveEntries()
maruel@chromium.org17cdf762010-05-28 17:30:52 +0000695 return 0
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000696
697 def PrintRevInfo(self):
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000698 """Output revision info mapping for the client and its dependencies.
699
700 This allows the capture of an overall "revision" for the source tree that
maruel@chromium.org918a9ae2010-05-28 15:50:30 +0000701 can be used to reproduce the same tree in the future. It is only useful for
702 "unpinned dependencies", i.e. DEPS/deps references without a svn revision
703 number or a git hash. A git branch name isn't "pinned" since the actual
704 commit can change.
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000705
706 The --snapshot option allows creating a .gclient file to reproduce the tree.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000707 """
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000708 if not self.dependencies:
maruel@chromium.org73e21142010-07-05 13:32:01 +0000709 raise gclient_utils.Error('No solution specified')
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000710 # Load all the settings.
711 self.RunCommandRecursively(self._options, {}, None, [], None)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000712
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000713 def GetURLAndRev(name, original_url):
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000714 """Returns the revision-qualified SCM url."""
715 if original_url is None:
maruel@chromium.orgbaa578e2010-07-12 17:36:59 +0000716 return None
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000717 if isinstance(original_url, self.FileImpl):
718 original_url = original_url.file_location
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000719 url, _ = gclient_utils.SplitUrlRevision(original_url)
maruel@chromium.org75a59272010-06-11 22:34:03 +0000720 scm = gclient_scm.CreateSCM(original_url, self.root_dir(), name)
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000721 if not os.path.isdir(scm.checkout_path):
722 return None
maruel@chromium.orgbaa578e2010-07-12 17:36:59 +0000723 return '%s@%s' % (url, scm.revinfo(self._options, [], None))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000724
maruel@chromium.orgbaa578e2010-07-12 17:36:59 +0000725 if self._options.snapshot:
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000726 new_gclient = ''
727 # First level at .gclient
728 for d in self.dependencies:
729 entries = {}
730 def GrabDeps(sol):
731 """Recursively grab dependencies."""
732 for i in sol.dependencies:
733 entries[i.name] = GetURLAndRev(i.name, i.parsed_url)
734 GrabDeps(i)
735 GrabDeps(d)
736 custom_deps = []
737 for k in sorted(entries.keys()):
738 if entries[k]:
739 # Quotes aren't escaped...
740 custom_deps.append(' \"%s\": \'%s\',\n' % (k, entries[k]))
741 else:
742 custom_deps.append(' \"%s\": None,\n' % k)
743 new_gclient += self.DEFAULT_SNAPSHOT_SOLUTION_TEXT % {
744 'solution_name': d.name,
745 'solution_url': d.url,
746 'safesync_url' : d.safesync_url or '',
747 'solution_deps': ''.join(custom_deps),
748 }
749 # Print the snapshot configuration file
750 print(self.DEFAULT_SNAPSHOT_FILE_TEXT % {'solution_list': new_gclient})
nasser@codeaurora.orgde8f3522010-03-11 23:47:44 +0000751 else:
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000752 entries = sorted(self.tree(False), key=lambda i: i.name)
753 for entry in entries:
754 entry_url = GetURLAndRev(entry.name, entry.parsed_url)
755 line = '%s: %s' % (entry.name, entry_url)
756 if not entry is entries[-1]:
757 line += ';'
758 print line
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000759
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000760 def ParseDepsFile(self, direct_reference):
761 """No DEPS to parse for a .gclient file."""
maruel@chromium.org98d05fa2010-07-22 21:58:01 +0000762 self.direct_reference = True
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000763 self.deps_parsed = True
764
maruel@chromium.org75a59272010-06-11 22:34:03 +0000765 def root_dir(self):
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000766 """Root directory of gclient checkout."""
maruel@chromium.org75a59272010-06-11 22:34:03 +0000767 return self._root_dir
768
maruel@chromium.org271375b2010-06-23 19:17:38 +0000769 def enforced_os(self):
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000770 """What deps_os entries that are to be parsed."""
maruel@chromium.org271375b2010-06-23 19:17:38 +0000771 return self._enforced_os
772
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000773 def recursion_limit(self):
774 """How recursive can each dependencies in DEPS file can load DEPS file."""
775 return self._recursion_limit
776
777 def tree(self, force_all):
778 """Returns a flat list of all the dependencies."""
maruel@chromium.orgc57e4f22010-07-22 21:37:46 +0000779 return self.subtree(force_all)
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000780
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000781
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000782#### gclient commands.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000783
784
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000785def CMDcleanup(parser, args):
786 """Cleans up all working copies.
maruel@chromium.orgddff62d2010-05-17 21:02:36 +0000787
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000788Mostly svn-specific. Simply runs 'svn cleanup' for each module.
maruel@chromium.org79692d62010-05-14 18:57:13 +0000789"""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000790 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
791 help='override deps for the specified (comma-separated) '
792 'platform(s); \'all\' will process all deps_os '
793 'references')
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000794 (options, args) = parser.parse_args(args)
maruel@chromium.org2806acc2009-05-15 12:33:34 +0000795 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000796 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000797 raise gclient_utils.Error('client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000798 if options.verbose:
799 # Print out the .gclient file. This is longer than if we just printed the
800 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.org116704f2010-06-11 17:34:38 +0000801 print(client.config_content)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000802 return client.RunOnDeps('cleanup', args)
803
804
piman@chromium.org4b90e3a2010-07-01 20:28:26 +0000805@attr('usage', '[command] [args ...]')
806def CMDrecurse(parser, args):
807 """Operates on all the entries.
808
809 Runs a shell command on all entries.
810 """
811 # Stop parsing at the first non-arg so that these go through to the command
812 parser.disable_interspersed_args()
813 parser.add_option('-s', '--scm', action='append', default=[],
814 help='choose scm types to operate upon')
815 options, args = parser.parse_args(args)
816 root, entries = gclient_utils.GetGClientRootAndEntries()
817 scm_set = set()
818 for scm in options.scm:
819 scm_set.update(scm.split(','))
820
821 # Pass in the SCM type as an env variable
822 env = os.environ.copy()
823
824 for path, url in entries.iteritems():
825 scm = gclient_scm.GetScmName(url)
826 if scm_set and scm not in scm_set:
827 continue
828 dir = os.path.normpath(os.path.join(root, path))
829 env['GCLIENT_SCM'] = scm
830 env['GCLIENT_URL'] = url
831 subprocess.Popen(args, cwd=dir, env=env).communicate()
832
833
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000834@attr('usage', '[url] [safesync url]')
835def CMDconfig(parser, args):
maruel@chromium.orgddff62d2010-05-17 21:02:36 +0000836 """Create a .gclient file in the current directory.
837
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000838This specifies the configuration for further commands. After update/sync,
maruel@chromium.org79692d62010-05-14 18:57:13 +0000839top-level DEPS files in each module are read to determine dependent
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000840modules to operate on as well. If optional [url] parameter is
maruel@chromium.org79692d62010-05-14 18:57:13 +0000841provided, then configuration is read from a specified Subversion server
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000842URL.
maruel@chromium.org79692d62010-05-14 18:57:13 +0000843"""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000844 parser.add_option('--spec',
845 help='create a gclient file containing the provided '
846 'string. Due to Cygwin/Python brokenness, it '
847 'probably can\'t contain any newlines.')
848 parser.add_option('--name',
849 help='overrides the default name for the solution')
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000850 (options, args) = parser.parse_args(args)
maruel@chromium.org5fc2a332010-05-26 19:37:15 +0000851 if ((options.spec and args) or len(args) > 2 or
852 (not options.spec and not args)):
853 parser.error('Inconsistent arguments. Use either --spec or one or 2 args')
854
maruel@chromium.org0329e672009-05-13 18:41:04 +0000855 if os.path.exists(options.config_filename):
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000856 raise gclient_utils.Error('%s file already exists in the current directory'
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000857 % options.config_filename)
maruel@chromium.org2806acc2009-05-15 12:33:34 +0000858 client = GClient('.', options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000859 if options.spec:
860 client.SetConfig(options.spec)
861 else:
maruel@chromium.org1ab7ffc2009-06-03 17:21:37 +0000862 base_url = args[0].rstrip('/')
iposva@chromium.org8cf7a392010-04-07 17:20:26 +0000863 if not options.name:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000864 name = base_url.split('/')[-1]
iposva@chromium.org8cf7a392010-04-07 17:20:26 +0000865 else:
866 # specify an alternate relpath for the given URL.
867 name = options.name
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000868 safesync_url = ''
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000869 if len(args) > 1:
870 safesync_url = args[1]
871 client.SetDefaultConfig(name, base_url, safesync_url)
872 client.SaveConfig()
maruel@chromium.org79692d62010-05-14 18:57:13 +0000873 return 0
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000874
875
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000876def CMDexport(parser, args):
maruel@chromium.orgddff62d2010-05-17 21:02:36 +0000877 """Wrapper for svn export for all managed directories."""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000878 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
879 help='override deps for the specified (comma-separated) '
880 'platform(s); \'all\' will process all deps_os '
881 'references')
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000882 (options, args) = parser.parse_args(args)
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +0000883 if len(args) != 1:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000884 raise gclient_utils.Error('Need directory name')
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +0000885 client = GClient.LoadCurrentConfig(options)
886
887 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000888 raise gclient_utils.Error('client not configured; see \'gclient config\'')
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +0000889
890 if options.verbose:
891 # Print out the .gclient file. This is longer than if we just printed the
892 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.org116704f2010-06-11 17:34:38 +0000893 print(client.config_content)
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +0000894 return client.RunOnDeps('export', args)
895
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000896
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000897@attr('epilog', """Example:
898 gclient pack > patch.txt
899 generate simple patch for configured client and dependences
900""")
901def CMDpack(parser, args):
maruel@chromium.org79692d62010-05-14 18:57:13 +0000902 """Generate a patch which can be applied at the root of the tree.
maruel@chromium.orgddff62d2010-05-17 21:02:36 +0000903
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000904Internally, runs 'svn diff'/'git diff' on each checked out module and
maruel@chromium.org79692d62010-05-14 18:57:13 +0000905dependencies, and performs minimal postprocessing of the output. The
906resulting patch is printed to stdout and can be applied to a freshly
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000907checked out tree via 'patch -p0 < patchfile'.
maruel@chromium.org79692d62010-05-14 18:57:13 +0000908"""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000909 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
910 help='override deps for the specified (comma-separated) '
911 'platform(s); \'all\' will process all deps_os '
912 'references')
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000913 (options, args) = parser.parse_args(args)
kbr@google.comab318592009-09-04 00:54:55 +0000914 client = GClient.LoadCurrentConfig(options)
915 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000916 raise gclient_utils.Error('client not configured; see \'gclient config\'')
kbr@google.comab318592009-09-04 00:54:55 +0000917 if options.verbose:
918 # Print out the .gclient file. This is longer than if we just printed the
919 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.org116704f2010-06-11 17:34:38 +0000920 print(client.config_content)
kbr@google.comab318592009-09-04 00:54:55 +0000921 return client.RunOnDeps('pack', args)
922
923
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000924def CMDstatus(parser, args):
925 """Show modification status for every dependencies."""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000926 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
927 help='override deps for the specified (comma-separated) '
928 'platform(s); \'all\' will process all deps_os '
929 'references')
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000930 (options, args) = parser.parse_args(args)
maruel@chromium.org2806acc2009-05-15 12:33:34 +0000931 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000932 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000933 raise gclient_utils.Error('client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000934 if options.verbose:
935 # Print out the .gclient file. This is longer than if we just printed the
936 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.org116704f2010-06-11 17:34:38 +0000937 print(client.config_content)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000938 return client.RunOnDeps('status', args)
939
940
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000941@attr('epilog', """Examples:
maruel@chromium.org79692d62010-05-14 18:57:13 +0000942 gclient sync
943 update files from SCM according to current configuration,
944 *for modules which have changed since last update or sync*
945 gclient sync --force
946 update files from SCM according to current configuration, for
947 all modules (useful for recovering files deleted from local copy)
948 gclient sync --revision src@31000
949 update src directory to r31000
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000950""")
951def CMDsync(parser, args):
952 """Checkout/update all modules."""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000953 parser.add_option('-f', '--force', action='store_true',
954 help='force update even for unchanged modules')
955 parser.add_option('-n', '--nohooks', action='store_true',
956 help='don\'t run hooks after the update is complete')
957 parser.add_option('-r', '--revision', action='append',
958 dest='revisions', metavar='REV', default=[],
959 help='Enforces revision/hash for the solutions with the '
960 'format src@rev. The src@ part is optional and can be '
961 'skipped. -r can be used multiple times when .gclient '
962 'has multiple solutions configured and will work even '
963 'if the src@ part is skipped.')
964 parser.add_option('-H', '--head', action='store_true',
965 help='skips any safesync_urls specified in '
966 'configured solutions and sync to head instead')
967 parser.add_option('-D', '--delete_unversioned_trees', action='store_true',
968 help='delete any unexpected unversioned trees '
969 'that are in the checkout')
970 parser.add_option('-R', '--reset', action='store_true',
971 help='resets any local changes before updating (git only)')
972 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
973 help='override deps for the specified (comma-separated) '
974 'platform(s); \'all\' will process all deps_os '
975 'references')
976 parser.add_option('-m', '--manually_grab_svn_rev', action='store_true',
977 help='Skip svn up whenever possible by requesting '
978 'actual HEAD revision from the repository')
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000979 (options, args) = parser.parse_args(args)
maruel@chromium.org2806acc2009-05-15 12:33:34 +0000980 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000981
982 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000983 raise gclient_utils.Error('client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000984
maruel@chromium.org307d1792010-05-31 20:03:13 +0000985 if options.revisions and options.head:
986 # TODO(maruel): Make it a parser.error if it doesn't break any builder.
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000987 print('Warning: you cannot use both --head and --revision')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000988
989 if options.verbose:
990 # Print out the .gclient file. This is longer than if we just printed the
991 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.org116704f2010-06-11 17:34:38 +0000992 print(client.config_content)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000993 return client.RunOnDeps('update', args)
994
995
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000996def CMDupdate(parser, args):
maruel@chromium.orgddff62d2010-05-17 21:02:36 +0000997 """Alias for the sync command. Deprecated."""
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000998 return CMDsync(parser, args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000999
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001000def CMDdiff(parser, args):
1001 """Displays local diff for every dependencies."""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00001002 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
1003 help='override deps for the specified (comma-separated) '
1004 'platform(s); \'all\' will process all deps_os '
1005 'references')
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001006 (options, args) = parser.parse_args(args)
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001007 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001008 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00001009 raise gclient_utils.Error('client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001010 if options.verbose:
1011 # Print out the .gclient file. This is longer than if we just printed the
1012 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.org116704f2010-06-11 17:34:38 +00001013 print(client.config_content)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001014 return client.RunOnDeps('diff', args)
1015
1016
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001017def CMDrevert(parser, args):
1018 """Revert all modifications in every dependencies."""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00001019 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
1020 help='override deps for the specified (comma-separated) '
1021 'platform(s); \'all\' will process all deps_os '
1022 'references')
1023 parser.add_option('-n', '--nohooks', action='store_true',
1024 help='don\'t run hooks after the revert is complete')
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001025 (options, args) = parser.parse_args(args)
1026 # --force is implied.
1027 options.force = True
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001028 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001029 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00001030 raise gclient_utils.Error('client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001031 return client.RunOnDeps('revert', args)
1032
1033
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001034def CMDrunhooks(parser, args):
1035 """Runs hooks for files that have been modified in the local working copy."""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00001036 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
1037 help='override deps for the specified (comma-separated) '
1038 'platform(s); \'all\' will process all deps_os '
1039 'references')
1040 parser.add_option('-f', '--force', action='store_true', default=True,
1041 help='Deprecated. No effect.')
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001042 (options, args) = parser.parse_args(args)
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001043 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001044 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00001045 raise gclient_utils.Error('client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001046 if options.verbose:
1047 # Print out the .gclient file. This is longer than if we just printed the
1048 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.org116704f2010-06-11 17:34:38 +00001049 print(client.config_content)
maruel@chromium.org5df6a462009-08-28 18:52:26 +00001050 options.force = True
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001051 options.nohooks = False
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001052 return client.RunOnDeps('runhooks', args)
1053
1054
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001055def CMDrevinfo(parser, args):
maruel@chromium.org9eda4112010-06-11 18:56:10 +00001056 """Output revision info mapping for the client and its dependencies.
1057
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00001058 This allows the capture of an overall 'revision' for the source tree that
maruel@chromium.org9eda4112010-06-11 18:56:10 +00001059 can be used to reproduce the same tree in the future. It is only useful for
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00001060 'unpinned dependencies', i.e. DEPS/deps references without a svn revision
1061 number or a git hash. A git branch name isn't 'pinned' since the actual
maruel@chromium.org9eda4112010-06-11 18:56:10 +00001062 commit can change.
1063 """
1064 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
1065 help='override deps for the specified (comma-separated) '
1066 'platform(s); \'all\' will process all deps_os '
1067 'references')
1068 parser.add_option('-s', '--snapshot', action='store_true',
1069 help='creates a snapshot .gclient file of the current '
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +00001070 'version of all repositories to reproduce the tree')
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001071 (options, args) = parser.parse_args(args)
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001072 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001073 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00001074 raise gclient_utils.Error('client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001075 client.PrintRevInfo()
maruel@chromium.org79692d62010-05-14 18:57:13 +00001076 return 0
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001077
1078
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001079def Command(name):
1080 return getattr(sys.modules[__name__], 'CMD' + name, None)
1081
1082
1083def CMDhelp(parser, args):
1084 """Prints list of commands or help for a specific command."""
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001085 (_, args) = parser.parse_args(args)
maruel@chromium.orgddff62d2010-05-17 21:02:36 +00001086 if len(args) == 1:
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001087 return Main(args + ['--help'])
maruel@chromium.orgddff62d2010-05-17 21:02:36 +00001088 parser.print_help()
1089 return 0
1090
1091
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001092def GenUsage(parser, command):
1093 """Modify an OptParse object with the function's documentation."""
1094 obj = Command(command)
1095 if command == 'help':
1096 command = '<command>'
1097 # OptParser.description prefer nicely non-formatted strings.
1098 parser.description = re.sub('[\r\n ]{2,}', ' ', obj.__doc__)
1099 usage = getattr(obj, 'usage', '')
1100 parser.set_usage('%%prog %s [options] %s' % (command, usage))
1101 parser.epilog = getattr(obj, 'epilog', None)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001102
1103
1104def Main(argv):
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001105 """Doesn't parse the arguments here, just find the right subcommand to
1106 execute."""
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001107 try:
1108 # Do it late so all commands are listed.
1109 CMDhelp.usage = ('\n\nCommands are:\n' + '\n'.join([
1110 ' %-10s %s' % (fn[3:], Command(fn[3:]).__doc__.split('\n')[0].strip())
1111 for fn in dir(sys.modules[__name__]) if fn.startswith('CMD')]))
1112 parser = optparse.OptionParser(version='%prog ' + __version__)
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001113 parser.add_option('-v', '--verbose', action='count', default=0,
1114 help='Produces additional output for diagnostics. Can be '
1115 'used up to three times for more logging info.')
1116 parser.add_option('--gclientfile', dest='config_filename',
1117 default=os.environ.get('GCLIENT_FILE', '.gclient'),
1118 help='Specify an alternate %default file')
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001119 # Integrate standard options processing.
1120 old_parser = parser.parse_args
1121 def Parse(args):
1122 (options, args) = old_parser(args)
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001123 level = None
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001124 if options.verbose == 2:
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001125 level = logging.INFO
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001126 elif options.verbose > 2:
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001127 level = logging.DEBUG
1128 logging.basicConfig(level=level,
1129 format='%(module)s(%(lineno)d) %(funcName)s:%(message)s')
1130 options.entries_filename = options.config_filename + '_entries'
maruel@chromium.orge3216c62010-07-08 03:31:43 +00001131
1132 # These hacks need to die.
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001133 if not hasattr(options, 'revisions'):
1134 # GClient.RunOnDeps expects it even if not applicable.
1135 options.revisions = []
1136 if not hasattr(options, 'head'):
1137 options.head = None
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001138 if not hasattr(options, 'nohooks'):
1139 options.nohooks = True
1140 if not hasattr(options, 'deps_os'):
1141 options.deps_os = None
maruel@chromium.orge3216c62010-07-08 03:31:43 +00001142 if not hasattr(options, 'manually_grab_svn_rev'):
1143 options.manually_grab_svn_rev = None
1144 if not hasattr(options, 'force'):
1145 options.force = None
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001146 return (options, args)
1147 parser.parse_args = Parse
1148 # We don't want wordwrapping in epilog (usually examples)
1149 parser.format_epilog = lambda _: parser.epilog or ''
1150 if argv:
1151 command = Command(argv[0])
1152 if command:
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001153 # 'fix' the usage and the description now that we know the subcommand.
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001154 GenUsage(parser, argv[0])
1155 return command(parser, argv[1:])
1156 # Not a known command. Default to help.
1157 GenUsage(parser, 'help')
1158 return CMDhelp(parser, argv)
1159 except gclient_utils.Error, e:
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001160 print >> sys.stderr, 'Error: %s' % str(e)
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001161 return 1
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001162
1163
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001164if '__main__' == __name__:
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001165 sys.exit(Main(sys.argv[1:]))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001166
1167# vim: ts=2:sw=2:tw=80:et: