blob: bf7b6ad6d998b65346d66280de455d9734b686aa [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.orgdefec8e2010-07-23 04:06:57 +0000168 # TODO(maruel): http://crbug.com/50015 Reenable this check once
169 # self.tree(False) is corrected.
170 # tree = dict((d.name, d) for d in self.tree(False))
171 #if self.name in tree:
172 # raise gclient_utils.Error(
173 # 'Dependency %s specified more than once:\n %s\nvs\n %s' %
174 # (self.name, tree[self.name].hierarchy(), self.hierarchy()))
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000175 if not isinstance(self.url,
176 (basestring, self.FromImpl, self.FileImpl, None.__class__)):
177 raise gclient_utils.Error('dependency url must be either a string, None, '
178 'File() or From() instead of %s' %
179 self.url.__class__.__name__)
180 if '/' in self.deps_file or '\\' in self.deps_file:
181 raise gclient_utils.Error('deps_file name must not be a path, just a '
182 'filename. %s' % self.deps_file)
183
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000184 def LateOverride(self, url):
185 overriden_url = self.get_custom_deps(self.name, url)
186 if overriden_url != url:
187 self.parsed_url = overriden_url
188 logging.debug('%s, %s was overriden to %s' % (self.name, url,
189 self.parsed_url))
190 elif isinstance(url, self.FromImpl):
191 ref = [dep for dep in self.tree(True) if url.module_name == dep.name]
192 if not len(ref) == 1:
193 raise Exception('Failed to find one reference to %s. %s' % (
194 url.module_name, ref))
195 ref = ref[0]
maruel@chromium.org98d05fa2010-07-22 21:58:01 +0000196 sub_target = url.sub_target_name or self.name
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000197 # Make sure the referenced dependency DEPS file is loaded and file the
198 # inner referenced dependency.
199 ref.ParseDepsFile(False)
200 found_dep = None
201 for d in ref.dependencies:
202 if d.name == sub_target:
203 found_dep = d
204 break
205 if not found_dep:
206 raise Exception('Couldn\'t find %s in %s, referenced by %s' % (
207 sub_target, ref.name, self.name))
208 # Call LateOverride() again.
209 self.parsed_url = found_dep.LateOverride(found_dep.url)
210 logging.debug('%s, %s to %s' % (self.name, url, self.parsed_url))
211 elif isinstance(url, basestring):
212 parsed_url = urlparse.urlparse(url)
213 if not parsed_url[0]:
214 # A relative url. Fetch the real base.
215 path = parsed_url[2]
216 if not path.startswith('/'):
217 raise gclient_utils.Error(
218 'relative DEPS entry \'%s\' must begin with a slash' % url)
219 # Create a scm just to query the full url.
220 parent_url = self.parent.parsed_url
221 if isinstance(parent_url, self.FileImpl):
222 parent_url = parent_url.file_location
223 scm = gclient_scm.CreateSCM(parent_url, self.root_dir(), None)
224 self.parsed_url = scm.FullUrlForRelativeUrl(url)
225 else:
226 self.parsed_url = url
227 logging.debug('%s, %s -> %s' % (self.name, url, self.parsed_url))
228 elif isinstance(url, self.FileImpl):
229 self.parsed_url = url
230 logging.debug('%s, %s -> %s (File)' % (self.name, url, self.parsed_url))
231 return self.parsed_url
232
maruel@chromium.org271375b2010-06-23 19:17:38 +0000233 def ParseDepsFile(self, direct_reference):
234 """Parses the DEPS file for this dependency."""
235 if direct_reference:
236 # Maybe it was referenced earlier by a From() keyword but it's now
237 # directly referenced.
238 self.direct_reference = direct_reference
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000239 if self.deps_parsed:
240 return
maruel@chromium.org271375b2010-06-23 19:17:38 +0000241 self.deps_parsed = True
242 filepath = os.path.join(self.root_dir(), self.name, self.deps_file)
243 if not os.path.isfile(filepath):
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000244 return
maruel@chromium.org271375b2010-06-23 19:17:38 +0000245 deps_content = gclient_utils.FileRead(filepath)
maruel@chromium.org0d425922010-06-21 19:22:24 +0000246
maruel@chromium.org271375b2010-06-23 19:17:38 +0000247 # Eval the content.
248 # One thing is unintuitive, vars= {} must happen before Var() use.
249 local_scope = {}
250 var = self.VarImpl(self.custom_vars, local_scope)
251 global_scope = {
252 'File': self.FileImpl,
253 'From': self.FromImpl,
254 'Var': var.Lookup,
255 'deps_os': {},
256 }
maruel@chromium.org5990f9d2010-07-07 18:02:58 +0000257 try:
258 exec(deps_content, global_scope, local_scope)
259 except SyntaxError, e:
260 gclient_utils.SyntaxErrorToError(filepath, e)
maruel@chromium.org271375b2010-06-23 19:17:38 +0000261 deps = local_scope.get('deps', {})
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000262 # load os specific dependencies if defined. these dependencies may
263 # override or extend the values defined by the 'deps' member.
maruel@chromium.org271375b2010-06-23 19:17:38 +0000264 if 'deps_os' in local_scope:
265 for deps_os_key in self.enforced_os():
266 os_deps = local_scope['deps_os'].get(deps_os_key, {})
267 if len(self.enforced_os()) > 1:
268 # Ignore any conflict when including deps for more than one
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000269 # platform, so we collect the broadest set of dependencies available.
270 # We may end up with the wrong revision of something for our
271 # platform, but this is the best we can do.
272 deps.update([x for x in os_deps.items() if not x[0] in deps])
273 else:
274 deps.update(os_deps)
275
maruel@chromium.org271375b2010-06-23 19:17:38 +0000276 self.deps_hooks.extend(local_scope.get('hooks', []))
277
278 # If a line is in custom_deps, but not in the solution, we want to append
279 # this line to the solution.
280 for d in self.custom_deps:
281 if d not in deps:
282 deps[d] = self.custom_deps[d]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000283
284 # If use_relative_paths is set in the DEPS file, regenerate
285 # the dictionary using paths relative to the directory containing
286 # the DEPS file.
maruel@chromium.org271375b2010-06-23 19:17:38 +0000287 use_relative_paths = local_scope.get('use_relative_paths', False)
288 if use_relative_paths:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000289 rel_deps = {}
290 for d, url in deps.items():
291 # normpath is required to allow DEPS to use .. in their
292 # dependency local path.
maruel@chromium.org271375b2010-06-23 19:17:38 +0000293 rel_deps[os.path.normpath(os.path.join(self.name, d))] = url
294 deps = rel_deps
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000295
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000296 # Convert the deps into real Dependency.
297 for name, url in deps.iteritems():
298 if name in [s.name for s in self.dependencies]:
299 raise
300 self.dependencies.append(Dependency(self, name, url))
bradnelson@google.com5f8f2a82010-07-23 00:05:46 +0000301 # Sort by name.
302 self.dependencies.sort(key=lambda x: x.name)
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000303 logging.info('Loaded: %s' % str(self))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000304
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000305 def RunCommandRecursively(self, options, revision_overrides,
306 command, args, pm):
307 """Runs 'command' before parsing the DEPS in case it's a initial checkout
308 or a revert."""
maruel@chromium.org861fd0f2010-07-23 03:05:05 +0000309 assert self._file_list == []
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000310 # When running runhooks, there's no need to consult the SCM.
311 # All known hooks are expected to run unconditionally regardless of working
312 # copy state, so skip the SCM status check.
313 run_scm = command not in ('runhooks', None)
314 self.LateOverride(self.url)
315 if run_scm and self.parsed_url:
316 if isinstance(self.parsed_url, self.FileImpl):
317 # Special support for single-file checkout.
318 if not command in (None, 'cleanup', 'diff', 'pack', 'status'):
319 options.revision = self.parsed_url.GetRevision()
320 scm = gclient_scm.SVNWrapper(self.parsed_url.GetPath(),
321 self.root_dir(),
322 self.name)
323 scm.RunCommand('updatesingle', options,
324 args + [self.parsed_url.GetFilename()],
maruel@chromium.org861fd0f2010-07-23 03:05:05 +0000325 self._file_list)
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000326 else:
327 options.revision = revision_overrides.get(self.name)
328 scm = gclient_scm.CreateSCM(self.parsed_url, self.root_dir(), self.name)
maruel@chromium.org861fd0f2010-07-23 03:05:05 +0000329 scm.RunCommand(command, options, args, self._file_list)
330 self._file_list = [os.path.join(self.name, f.strip())
331 for f in self._file_list]
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000332 options.revision = None
333 if pm:
334 # The + 1 comes from the fact that .gclient is considered a step in
335 # itself, .i.e. this code is called one time for the .gclient. This is not
336 # conceptually correct but it simplifies code.
337 pm._total = len(self.tree(False)) + 1
338 pm.update()
339 if self.recursion_limit():
340 # Then we can parse the DEPS file.
341 self.ParseDepsFile(True)
342 if pm:
343 pm._total = len(self.tree(False)) + 1
344 pm.update(0)
345 # Parse the dependencies of this dependency.
346 for s in self.dependencies:
347 # TODO(maruel): All these can run concurrently! No need for threads,
348 # just buffer stdout&stderr on pipes and flush as they complete.
349 # Watch out for stdin.
350 s.RunCommandRecursively(options, revision_overrides, command, args, pm)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000351
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000352 def RunHooksRecursively(self, options):
353 """Evaluates all hooks, running actions as needed. RunCommandRecursively()
354 must have been called before to load the DEPS."""
maruel@chromium.orgdc7445d2010-07-09 21:05:29 +0000355 # If "--force" was specified, run all hooks regardless of what files have
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000356 # changed.
357 if self.deps_hooks and self.direct_reference:
358 # TODO(maruel): If the user is using git or git-svn, then we don't know
359 # what files have changed so we always run all hooks. It'd be nice to fix
360 # that.
361 if (options.force or
362 isinstance(self.parsed_url, self.FileImpl) or
363 gclient_scm.GetScmName(self.parsed_url) in ('git', None) or
364 os.path.isdir(os.path.join(self.root_dir(), self.name, '.git'))):
365 for hook_dict in self.deps_hooks:
366 self._RunHookAction(hook_dict, [])
367 else:
368 # TODO(phajdan.jr): We should know exactly when the paths are absolute.
369 # Convert all absolute paths to relative.
maruel@chromium.org861fd0f2010-07-23 03:05:05 +0000370 file_list = self.file_list()
371 for i in range(len(file_list)):
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000372 # It depends on the command being executed (like runhooks vs sync).
maruel@chromium.org861fd0f2010-07-23 03:05:05 +0000373 if not os.path.isabs(file_list[i]):
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000374 continue
maruel@chromium.orgdc7445d2010-07-09 21:05:29 +0000375
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000376 prefix = os.path.commonprefix([self.root_dir().lower(),
maruel@chromium.org861fd0f2010-07-23 03:05:05 +0000377 file_list[i].lower()])
378 file_list[i] = file_list[i][len(prefix):]
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000379
380 # Strip any leading path separators.
maruel@chromium.org861fd0f2010-07-23 03:05:05 +0000381 while (file_list[i].startswith('\\') or
382 file_list[i].startswith('/')):
383 file_list[i] = file_list[i][1:]
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000384
385 # Run hooks on the basis of whether the files from the gclient operation
386 # match each hook's pattern.
387 for hook_dict in self.deps_hooks:
388 pattern = re.compile(hook_dict['pattern'])
maruel@chromium.org861fd0f2010-07-23 03:05:05 +0000389 matching_file_list = [f for f in file_list if pattern.search(f)]
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000390 if matching_file_list:
391 self._RunHookAction(hook_dict, matching_file_list)
392 if self.recursion_limit():
393 for s in self.dependencies:
394 s.RunHooksRecursively(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000395
maruel@chromium.orgeaf61062010-07-07 18:42:39 +0000396 def _RunHookAction(self, hook_dict, matching_file_list):
397 """Runs the action from a single hook."""
398 logging.info(hook_dict)
399 logging.info(matching_file_list)
400 command = hook_dict['action'][:]
401 if command[0] == 'python':
402 # If the hook specified "python" as the first item, the action is a
403 # Python script. Run it by starting a new copy of the same
404 # interpreter.
405 command[0] = sys.executable
406
407 if '$matching_files' in command:
408 splice_index = command.index('$matching_files')
409 command[splice_index:splice_index + 1] = matching_file_list
410
411 # Use a discrete exit status code of 2 to indicate that a hook action
412 # failed. Users of this script may wish to treat hook action failures
413 # differently from VC failures.
414 return gclient_utils.SubprocessCall(command, self.root_dir(), fail_status=2)
415
maruel@chromium.org271375b2010-06-23 19:17:38 +0000416 def root_dir(self):
417 return self.parent.root_dir()
418
419 def enforced_os(self):
420 return self.parent.enforced_os()
421
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000422 def recursion_limit(self):
423 return self.parent.recursion_limit() - 1
424
425 def tree(self, force_all):
426 return self.parent.tree(force_all)
427
maruel@chromium.orgc57e4f22010-07-22 21:37:46 +0000428 def subtree(self, force_all):
429 result = []
430 # Add breadth-first.
431 if self.direct_reference or force_all:
432 for d in self.dependencies:
maruel@chromium.org044f4e32010-07-22 21:59:57 +0000433 result.append(d)
maruel@chromium.orgc57e4f22010-07-22 21:37:46 +0000434 for d in self.dependencies:
435 result.extend(d.subtree(force_all))
436 return result
437
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000438 def get_custom_deps(self, name, url):
439 """Returns a custom deps if applicable."""
440 if self.parent:
441 url = self.parent.get_custom_deps(name, url)
442 # None is a valid return value to disable a dependency.
443 return self.custom_deps.get(name, url)
444
maruel@chromium.org861fd0f2010-07-23 03:05:05 +0000445 def file_list(self):
446 result = self._file_list[:]
447 for d in self.dependencies:
448 result.extend(d.file_list())
449 return result
450
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000451 def __str__(self):
452 out = []
453 for i in ('name', 'url', 'safesync_url', 'custom_deps', 'custom_vars',
maruel@chromium.org861fd0f2010-07-23 03:05:05 +0000454 'deps_hooks', '_file_list'):
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000455 # 'deps_file'
456 if self.__dict__[i]:
457 out.append('%s: %s' % (i, self.__dict__[i]))
458
459 for d in self.dependencies:
460 out.extend([' ' + x for x in str(d).splitlines()])
461 out.append('')
462 return '\n'.join(out)
463
464 def __repr__(self):
465 return '%s: %s' % (self.name, self.url)
466
maruel@chromium.orgbffb9042010-07-22 20:59:36 +0000467 def hierarchy(self):
maruel@chromium.orgbc2d2f92010-07-22 21:26:48 +0000468 """Returns a human-readable hierarchical reference to a Dependency."""
maruel@chromium.orgbffb9042010-07-22 20:59:36 +0000469 out = '%s(%s)' % (self.name, self.url)
470 i = self.parent
471 while i and i.name:
472 out = '%s(%s) -> %s' % (i.name, i.url, out)
473 i = i.parent
474 return out
475
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000476
477class GClient(Dependency):
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000478 """Object that represent a gclient checkout. A tree of Dependency(), one per
479 solution or DEPS entry."""
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000480
481 DEPS_OS_CHOICES = {
482 "win32": "win",
483 "win": "win",
484 "cygwin": "win",
485 "darwin": "mac",
486 "mac": "mac",
487 "unix": "unix",
488 "linux": "unix",
489 "linux2": "unix",
490 }
491
492 DEFAULT_CLIENT_FILE_TEXT = ("""\
493solutions = [
494 { "name" : "%(solution_name)s",
495 "url" : "%(solution_url)s",
496 "custom_deps" : {
497 },
maruel@chromium.org73e21142010-07-05 13:32:01 +0000498 "safesync_url": "%(safesync_url)s",
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000499 },
500]
501""")
502
503 DEFAULT_SNAPSHOT_SOLUTION_TEXT = ("""\
504 { "name" : "%(solution_name)s",
505 "url" : "%(solution_url)s",
506 "custom_deps" : {
maruel@chromium.org73e21142010-07-05 13:32:01 +0000507%(solution_deps)s },
508 "safesync_url": "%(safesync_url)s",
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000509 },
510""")
511
512 DEFAULT_SNAPSHOT_FILE_TEXT = ("""\
513# Snapshot generated with gclient revinfo --snapshot
514solutions = [
maruel@chromium.org73e21142010-07-05 13:32:01 +0000515%(solution_list)s]
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000516""")
517
518 def __init__(self, root_dir, options):
519 Dependency.__init__(self, None, None, None)
maruel@chromium.org0d425922010-06-21 19:22:24 +0000520 self._options = options
maruel@chromium.org271375b2010-06-23 19:17:38 +0000521 if options.deps_os:
522 enforced_os = options.deps_os.split(',')
523 else:
524 enforced_os = [self.DEPS_OS_CHOICES.get(sys.platform, 'unix')]
525 if 'all' in enforced_os:
526 enforced_os = self.DEPS_OS_CHOICES.itervalues()
527 self._enforced_os = list(set(enforced_os))
528 self._root_dir = root_dir
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000529 self.config_content = None
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000530 # Do not change previous behavior. Only solution level and immediate DEPS
531 # are processed.
532 self._recursion_limit = 2
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000533
534 def SetConfig(self, content):
535 assert self.dependencies == []
536 config_dict = {}
537 self.config_content = content
538 try:
539 exec(content, config_dict)
540 except SyntaxError, e:
maruel@chromium.org5990f9d2010-07-07 18:02:58 +0000541 gclient_utils.SyntaxErrorToError('.gclient', e)
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000542 for s in config_dict.get('solutions', []):
maruel@chromium.org81843b82010-06-28 16:49:26 +0000543 try:
544 self.dependencies.append(Dependency(
545 self, s['name'], s['url'],
546 s.get('safesync_url', None),
547 s.get('custom_deps', {}),
548 s.get('custom_vars', {})))
549 except KeyError:
550 raise gclient_utils.Error('Invalid .gclient file. Solution is '
551 'incomplete: %s' % s)
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000552 # .gclient can have hooks.
553 self.deps_hooks = config_dict.get('hooks', [])
554
555 def SaveConfig(self):
556 gclient_utils.FileWrite(os.path.join(self.root_dir(),
557 self._options.config_filename),
558 self.config_content)
559
560 @staticmethod
561 def LoadCurrentConfig(options):
562 """Searches for and loads a .gclient file relative to the current working
563 dir. Returns a GClient object."""
564 path = gclient_utils.FindGclientRoot(os.getcwd(), options.config_filename)
565 if not path:
566 return None
567 client = GClient(path, options)
568 client.SetConfig(gclient_utils.FileRead(
569 os.path.join(path, options.config_filename)))
570 return client
571
572 def SetDefaultConfig(self, solution_name, solution_url, safesync_url):
573 self.SetConfig(self.DEFAULT_CLIENT_FILE_TEXT % {
574 'solution_name': solution_name,
575 'solution_url': solution_url,
576 'safesync_url' : safesync_url,
577 })
578
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000579 def _SaveEntries(self):
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000580 """Creates a .gclient_entries file to record the list of unique checkouts.
581
582 The .gclient_entries file lives in the same directory as .gclient.
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000583 """
584 # Sometimes pprint.pformat will use {', sometimes it'll use { ' ... It
585 # makes testing a bit too fun.
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000586 result = 'entries = {\n'
587 for entry in self.tree(False):
588 # Skip over File() dependencies as we can't version them.
589 if not isinstance(entry.parsed_url, self.FileImpl):
590 result += ' %s: %s,\n' % (pprint.pformat(entry.name),
591 pprint.pformat(entry.parsed_url))
592 result += '}\n'
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000593 file_path = os.path.join(self.root_dir(), self._options.entries_filename)
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000594 logging.info(result)
595 gclient_utils.FileWrite(file_path, result)
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000596
597 def _ReadEntries(self):
598 """Read the .gclient_entries file for the given client.
599
600 Returns:
601 A sequence of solution names, which will be empty if there is the
602 entries file hasn't been created yet.
603 """
604 scope = {}
605 filename = os.path.join(self.root_dir(), self._options.entries_filename)
606 if not os.path.exists(filename):
maruel@chromium.org73e21142010-07-05 13:32:01 +0000607 return {}
maruel@chromium.org5990f9d2010-07-07 18:02:58 +0000608 try:
609 exec(gclient_utils.FileRead(filename), scope)
610 except SyntaxError, e:
611 gclient_utils.SyntaxErrorToError(filename, e)
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000612 return scope['entries']
613
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000614 def _EnforceRevisions(self):
maruel@chromium.org918a9ae2010-05-28 15:50:30 +0000615 """Checks for revision overrides."""
616 revision_overrides = {}
maruel@chromium.org307d1792010-05-31 20:03:13 +0000617 if self._options.head:
618 return revision_overrides
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000619 for s in self.dependencies:
620 if not s.safesync_url:
maruel@chromium.org307d1792010-05-31 20:03:13 +0000621 continue
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000622 handle = urllib.urlopen(s.safesync_url)
maruel@chromium.org307d1792010-05-31 20:03:13 +0000623 rev = handle.read().strip()
624 handle.close()
625 if len(rev):
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000626 self._options.revisions.append('%s@%s' % (s.name, rev))
maruel@chromium.org307d1792010-05-31 20:03:13 +0000627 if not self._options.revisions:
628 return revision_overrides
629 # --revision will take over safesync_url.
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000630 solutions_names = [s.name for s in self.dependencies]
maruel@chromium.org307d1792010-05-31 20:03:13 +0000631 index = 0
632 for revision in self._options.revisions:
633 if not '@' in revision:
634 # Support for --revision 123
635 revision = '%s@%s' % (solutions_names[index], revision)
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000636 sol, rev = revision.split('@', 1)
maruel@chromium.org307d1792010-05-31 20:03:13 +0000637 if not sol in solutions_names:
638 #raise gclient_utils.Error('%s is not a valid solution.' % sol)
639 print >> sys.stderr, ('Please fix your script, having invalid '
640 '--revision flags will soon considered an error.')
641 else:
maruel@chromium.org918a9ae2010-05-28 15:50:30 +0000642 revision_overrides[sol] = rev
maruel@chromium.org307d1792010-05-31 20:03:13 +0000643 index += 1
maruel@chromium.org918a9ae2010-05-28 15:50:30 +0000644 return revision_overrides
645
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000646 def RunOnDeps(self, command, args):
647 """Runs a command on each dependency in a client and its dependencies.
648
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000649 Args:
650 command: The command to use (e.g., 'status' or 'diff')
651 args: list of str - extra arguments to add to the command line.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000652 """
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000653 if not self.dependencies:
maruel@chromium.org73e21142010-07-05 13:32:01 +0000654 raise gclient_utils.Error('No solution specified')
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000655 revision_overrides = self._EnforceRevisions()
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000656 pm = None
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000657 if command == 'update' and not self._options.verbose:
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000658 pm = Progress('Syncing projects', len(self.tree(False)) + 1)
659 self.RunCommandRecursively(self._options, revision_overrides,
660 command, args, pm)
661 if pm:
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000662 pm.end()
piman@chromium.org6f363722010-04-27 00:41:09 +0000663
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000664 # Once all the dependencies have been processed, it's now safe to run the
665 # hooks.
666 if not self._options.nohooks:
667 self.RunHooksRecursively(self._options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000668
669 if command == 'update':
ajwong@chromium.orgcdcee802009-06-23 15:30:42 +0000670 # Notify the user if there is an orphaned entry in their working copy.
671 # Only delete the directory if there are no changes in it, and
672 # delete_unversioned_trees is set to true.
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000673 entries = [i.name for i in self.tree(False)]
674 for entry, prev_url in self._ReadEntries().iteritems():
maruel@chromium.orgc5e9aec2009-08-03 18:25:56 +0000675 # Fix path separator on Windows.
676 entry_fixed = entry.replace('/', os.path.sep)
maruel@chromium.org75a59272010-06-11 22:34:03 +0000677 e_dir = os.path.join(self.root_dir(), entry_fixed)
maruel@chromium.orgc5e9aec2009-08-03 18:25:56 +0000678 # Use entry and not entry_fixed there.
maruel@chromium.org0329e672009-05-13 18:41:04 +0000679 if entry not in entries and os.path.exists(e_dir):
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000680 file_list = []
681 scm = gclient_scm.CreateSCM(prev_url, self.root_dir(), entry_fixed)
682 scm.status(self._options, [], file_list)
683 modified_files = file_list != []
msb@chromium.org83017012009-09-28 18:52:12 +0000684 if not self._options.delete_unversioned_trees or modified_files:
maruel@chromium.orgc5e9aec2009-08-03 18:25:56 +0000685 # There are modified files in this entry. Keep warning until
686 # removed.
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000687 print(('\nWARNING: \'%s\' is no longer part of this client. '
688 'It is recommended that you manually remove it.\n') %
maruel@chromium.orgc5e9aec2009-08-03 18:25:56 +0000689 entry_fixed)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000690 else:
691 # Delete the entry
maruel@chromium.org73e21142010-07-05 13:32:01 +0000692 print('\n________ deleting \'%s\' in \'%s\'' % (
693 entry_fixed, self.root_dir()))
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000694 gclient_utils.RemoveDirectory(e_dir)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000695 # record the current list of entries for next time
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000696 self._SaveEntries()
maruel@chromium.org17cdf762010-05-28 17:30:52 +0000697 return 0
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000698
699 def PrintRevInfo(self):
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000700 """Output revision info mapping for the client and its dependencies.
701
702 This allows the capture of an overall "revision" for the source tree that
maruel@chromium.org918a9ae2010-05-28 15:50:30 +0000703 can be used to reproduce the same tree in the future. It is only useful for
704 "unpinned dependencies", i.e. DEPS/deps references without a svn revision
705 number or a git hash. A git branch name isn't "pinned" since the actual
706 commit can change.
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000707
708 The --snapshot option allows creating a .gclient file to reproduce the tree.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000709 """
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000710 if not self.dependencies:
maruel@chromium.org73e21142010-07-05 13:32:01 +0000711 raise gclient_utils.Error('No solution specified')
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000712 # Load all the settings.
713 self.RunCommandRecursively(self._options, {}, None, [], None)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000714
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000715 def GetURLAndRev(name, original_url):
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000716 """Returns the revision-qualified SCM url."""
717 if original_url is None:
maruel@chromium.orgbaa578e2010-07-12 17:36:59 +0000718 return None
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000719 if isinstance(original_url, self.FileImpl):
720 original_url = original_url.file_location
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000721 url, _ = gclient_utils.SplitUrlRevision(original_url)
maruel@chromium.org75a59272010-06-11 22:34:03 +0000722 scm = gclient_scm.CreateSCM(original_url, self.root_dir(), name)
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000723 if not os.path.isdir(scm.checkout_path):
724 return None
maruel@chromium.orgbaa578e2010-07-12 17:36:59 +0000725 return '%s@%s' % (url, scm.revinfo(self._options, [], None))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000726
maruel@chromium.orgbaa578e2010-07-12 17:36:59 +0000727 if self._options.snapshot:
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000728 new_gclient = ''
729 # First level at .gclient
730 for d in self.dependencies:
731 entries = {}
732 def GrabDeps(sol):
733 """Recursively grab dependencies."""
734 for i in sol.dependencies:
735 entries[i.name] = GetURLAndRev(i.name, i.parsed_url)
736 GrabDeps(i)
737 GrabDeps(d)
738 custom_deps = []
739 for k in sorted(entries.keys()):
740 if entries[k]:
741 # Quotes aren't escaped...
742 custom_deps.append(' \"%s\": \'%s\',\n' % (k, entries[k]))
743 else:
744 custom_deps.append(' \"%s\": None,\n' % k)
745 new_gclient += self.DEFAULT_SNAPSHOT_SOLUTION_TEXT % {
746 'solution_name': d.name,
747 'solution_url': d.url,
748 'safesync_url' : d.safesync_url or '',
749 'solution_deps': ''.join(custom_deps),
750 }
751 # Print the snapshot configuration file
752 print(self.DEFAULT_SNAPSHOT_FILE_TEXT % {'solution_list': new_gclient})
nasser@codeaurora.orgde8f3522010-03-11 23:47:44 +0000753 else:
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +0000754 entries = sorted(self.tree(False), key=lambda i: i.name)
755 for entry in entries:
756 entry_url = GetURLAndRev(entry.name, entry.parsed_url)
757 line = '%s: %s' % (entry.name, entry_url)
758 if not entry is entries[-1]:
759 line += ';'
760 print line
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000761
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000762 def ParseDepsFile(self, direct_reference):
763 """No DEPS to parse for a .gclient file."""
maruel@chromium.org98d05fa2010-07-22 21:58:01 +0000764 self.direct_reference = True
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000765 self.deps_parsed = True
766
maruel@chromium.org75a59272010-06-11 22:34:03 +0000767 def root_dir(self):
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000768 """Root directory of gclient checkout."""
maruel@chromium.org75a59272010-06-11 22:34:03 +0000769 return self._root_dir
770
maruel@chromium.org271375b2010-06-23 19:17:38 +0000771 def enforced_os(self):
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000772 """What deps_os entries that are to be parsed."""
maruel@chromium.org271375b2010-06-23 19:17:38 +0000773 return self._enforced_os
774
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000775 def recursion_limit(self):
776 """How recursive can each dependencies in DEPS file can load DEPS file."""
777 return self._recursion_limit
778
779 def tree(self, force_all):
780 """Returns a flat list of all the dependencies."""
maruel@chromium.orgc57e4f22010-07-22 21:37:46 +0000781 return self.subtree(force_all)
maruel@chromium.orgd36fba82010-06-28 16:50:40 +0000782
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000783
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000784#### gclient commands.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000785
786
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000787def CMDcleanup(parser, args):
788 """Cleans up all working copies.
maruel@chromium.orgddff62d2010-05-17 21:02:36 +0000789
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000790Mostly svn-specific. Simply runs 'svn cleanup' for each module.
maruel@chromium.org79692d62010-05-14 18:57:13 +0000791"""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000792 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
793 help='override deps for the specified (comma-separated) '
794 'platform(s); \'all\' will process all deps_os '
795 'references')
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000796 (options, args) = parser.parse_args(args)
maruel@chromium.org2806acc2009-05-15 12:33:34 +0000797 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000798 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000799 raise gclient_utils.Error('client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000800 if options.verbose:
801 # Print out the .gclient file. This is longer than if we just printed the
802 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.org116704f2010-06-11 17:34:38 +0000803 print(client.config_content)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000804 return client.RunOnDeps('cleanup', args)
805
806
piman@chromium.org4b90e3a2010-07-01 20:28:26 +0000807@attr('usage', '[command] [args ...]')
808def CMDrecurse(parser, args):
809 """Operates on all the entries.
810
811 Runs a shell command on all entries.
812 """
813 # Stop parsing at the first non-arg so that these go through to the command
814 parser.disable_interspersed_args()
815 parser.add_option('-s', '--scm', action='append', default=[],
816 help='choose scm types to operate upon')
817 options, args = parser.parse_args(args)
818 root, entries = gclient_utils.GetGClientRootAndEntries()
819 scm_set = set()
820 for scm in options.scm:
821 scm_set.update(scm.split(','))
822
823 # Pass in the SCM type as an env variable
824 env = os.environ.copy()
825
826 for path, url in entries.iteritems():
827 scm = gclient_scm.GetScmName(url)
828 if scm_set and scm not in scm_set:
829 continue
830 dir = os.path.normpath(os.path.join(root, path))
831 env['GCLIENT_SCM'] = scm
832 env['GCLIENT_URL'] = url
833 subprocess.Popen(args, cwd=dir, env=env).communicate()
834
835
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000836@attr('usage', '[url] [safesync url]')
837def CMDconfig(parser, args):
maruel@chromium.orgddff62d2010-05-17 21:02:36 +0000838 """Create a .gclient file in the current directory.
839
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000840This specifies the configuration for further commands. After update/sync,
maruel@chromium.org79692d62010-05-14 18:57:13 +0000841top-level DEPS files in each module are read to determine dependent
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000842modules to operate on as well. If optional [url] parameter is
maruel@chromium.org79692d62010-05-14 18:57:13 +0000843provided, then configuration is read from a specified Subversion server
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000844URL.
maruel@chromium.org79692d62010-05-14 18:57:13 +0000845"""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000846 parser.add_option('--spec',
847 help='create a gclient file containing the provided '
848 'string. Due to Cygwin/Python brokenness, it '
849 'probably can\'t contain any newlines.')
850 parser.add_option('--name',
851 help='overrides the default name for the solution')
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000852 (options, args) = parser.parse_args(args)
maruel@chromium.org5fc2a332010-05-26 19:37:15 +0000853 if ((options.spec and args) or len(args) > 2 or
854 (not options.spec and not args)):
855 parser.error('Inconsistent arguments. Use either --spec or one or 2 args')
856
maruel@chromium.org0329e672009-05-13 18:41:04 +0000857 if os.path.exists(options.config_filename):
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000858 raise gclient_utils.Error('%s file already exists in the current directory'
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000859 % options.config_filename)
maruel@chromium.org2806acc2009-05-15 12:33:34 +0000860 client = GClient('.', options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000861 if options.spec:
862 client.SetConfig(options.spec)
863 else:
maruel@chromium.org1ab7ffc2009-06-03 17:21:37 +0000864 base_url = args[0].rstrip('/')
iposva@chromium.org8cf7a392010-04-07 17:20:26 +0000865 if not options.name:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000866 name = base_url.split('/')[-1]
iposva@chromium.org8cf7a392010-04-07 17:20:26 +0000867 else:
868 # specify an alternate relpath for the given URL.
869 name = options.name
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000870 safesync_url = ''
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000871 if len(args) > 1:
872 safesync_url = args[1]
873 client.SetDefaultConfig(name, base_url, safesync_url)
874 client.SaveConfig()
maruel@chromium.org79692d62010-05-14 18:57:13 +0000875 return 0
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000876
877
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000878def CMDexport(parser, args):
maruel@chromium.orgddff62d2010-05-17 21:02:36 +0000879 """Wrapper for svn export for all managed directories."""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000880 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
881 help='override deps for the specified (comma-separated) '
882 'platform(s); \'all\' will process all deps_os '
883 'references')
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000884 (options, args) = parser.parse_args(args)
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +0000885 if len(args) != 1:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000886 raise gclient_utils.Error('Need directory name')
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +0000887 client = GClient.LoadCurrentConfig(options)
888
889 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000890 raise gclient_utils.Error('client not configured; see \'gclient config\'')
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +0000891
892 if options.verbose:
893 # Print out the .gclient file. This is longer than if we just printed the
894 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.org116704f2010-06-11 17:34:38 +0000895 print(client.config_content)
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +0000896 return client.RunOnDeps('export', args)
897
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000898
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000899@attr('epilog', """Example:
900 gclient pack > patch.txt
901 generate simple patch for configured client and dependences
902""")
903def CMDpack(parser, args):
maruel@chromium.org79692d62010-05-14 18:57:13 +0000904 """Generate a patch which can be applied at the root of the tree.
maruel@chromium.orgddff62d2010-05-17 21:02:36 +0000905
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000906Internally, runs 'svn diff'/'git diff' on each checked out module and
maruel@chromium.org79692d62010-05-14 18:57:13 +0000907dependencies, and performs minimal postprocessing of the output. The
908resulting patch is printed to stdout and can be applied to a freshly
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000909checked out tree via 'patch -p0 < patchfile'.
maruel@chromium.org79692d62010-05-14 18:57:13 +0000910"""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000911 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
912 help='override deps for the specified (comma-separated) '
913 'platform(s); \'all\' will process all deps_os '
914 'references')
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000915 (options, args) = parser.parse_args(args)
kbr@google.comab318592009-09-04 00:54:55 +0000916 client = GClient.LoadCurrentConfig(options)
917 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000918 raise gclient_utils.Error('client not configured; see \'gclient config\'')
kbr@google.comab318592009-09-04 00:54:55 +0000919 if options.verbose:
920 # Print out the .gclient file. This is longer than if we just printed the
921 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.org116704f2010-06-11 17:34:38 +0000922 print(client.config_content)
kbr@google.comab318592009-09-04 00:54:55 +0000923 return client.RunOnDeps('pack', args)
924
925
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000926def CMDstatus(parser, args):
927 """Show modification status for every dependencies."""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000928 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
929 help='override deps for the specified (comma-separated) '
930 'platform(s); \'all\' will process all deps_os '
931 'references')
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000932 (options, args) = parser.parse_args(args)
maruel@chromium.org2806acc2009-05-15 12:33:34 +0000933 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000934 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000935 raise gclient_utils.Error('client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000936 if options.verbose:
937 # Print out the .gclient file. This is longer than if we just printed the
938 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.org116704f2010-06-11 17:34:38 +0000939 print(client.config_content)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000940 return client.RunOnDeps('status', args)
941
942
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000943@attr('epilog', """Examples:
maruel@chromium.org79692d62010-05-14 18:57:13 +0000944 gclient sync
945 update files from SCM according to current configuration,
946 *for modules which have changed since last update or sync*
947 gclient sync --force
948 update files from SCM according to current configuration, for
949 all modules (useful for recovering files deleted from local copy)
950 gclient sync --revision src@31000
951 update src directory to r31000
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000952""")
953def CMDsync(parser, args):
954 """Checkout/update all modules."""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000955 parser.add_option('-f', '--force', action='store_true',
956 help='force update even for unchanged modules')
957 parser.add_option('-n', '--nohooks', action='store_true',
958 help='don\'t run hooks after the update is complete')
959 parser.add_option('-r', '--revision', action='append',
960 dest='revisions', metavar='REV', default=[],
961 help='Enforces revision/hash for the solutions with the '
962 'format src@rev. The src@ part is optional and can be '
963 'skipped. -r can be used multiple times when .gclient '
964 'has multiple solutions configured and will work even '
965 'if the src@ part is skipped.')
966 parser.add_option('-H', '--head', action='store_true',
967 help='skips any safesync_urls specified in '
968 'configured solutions and sync to head instead')
969 parser.add_option('-D', '--delete_unversioned_trees', action='store_true',
970 help='delete any unexpected unversioned trees '
971 'that are in the checkout')
972 parser.add_option('-R', '--reset', action='store_true',
973 help='resets any local changes before updating (git only)')
974 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
975 help='override deps for the specified (comma-separated) '
976 'platform(s); \'all\' will process all deps_os '
977 'references')
978 parser.add_option('-m', '--manually_grab_svn_rev', action='store_true',
979 help='Skip svn up whenever possible by requesting '
980 'actual HEAD revision from the repository')
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000981 (options, args) = parser.parse_args(args)
maruel@chromium.org2806acc2009-05-15 12:33:34 +0000982 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000983
984 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
maruel@chromium.org307d1792010-05-31 20:03:13 +0000987 if options.revisions and options.head:
988 # TODO(maruel): Make it a parser.error if it doesn't break any builder.
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000989 print('Warning: you cannot use both --head and --revision')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000990
991 if options.verbose:
992 # Print out the .gclient file. This is longer than if we just printed the
993 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.org116704f2010-06-11 17:34:38 +0000994 print(client.config_content)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000995 return client.RunOnDeps('update', args)
996
997
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000998def CMDupdate(parser, args):
maruel@chromium.orgddff62d2010-05-17 21:02:36 +0000999 """Alias for the sync command. Deprecated."""
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001000 return CMDsync(parser, args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001001
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001002def CMDdiff(parser, args):
1003 """Displays local diff for every dependencies."""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00001004 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
1005 help='override deps for the specified (comma-separated) '
1006 'platform(s); \'all\' will process all deps_os '
1007 'references')
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001008 (options, args) = parser.parse_args(args)
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001009 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001010 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00001011 raise gclient_utils.Error('client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001012 if options.verbose:
1013 # Print out the .gclient file. This is longer than if we just printed the
1014 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.org116704f2010-06-11 17:34:38 +00001015 print(client.config_content)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001016 return client.RunOnDeps('diff', args)
1017
1018
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001019def CMDrevert(parser, args):
1020 """Revert all modifications in every dependencies."""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00001021 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
1022 help='override deps for the specified (comma-separated) '
1023 'platform(s); \'all\' will process all deps_os '
1024 'references')
1025 parser.add_option('-n', '--nohooks', action='store_true',
1026 help='don\'t run hooks after the revert is complete')
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001027 (options, args) = parser.parse_args(args)
1028 # --force is implied.
1029 options.force = True
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001030 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001031 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00001032 raise gclient_utils.Error('client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001033 return client.RunOnDeps('revert', args)
1034
1035
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001036def CMDrunhooks(parser, args):
1037 """Runs hooks for files that have been modified in the local working copy."""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00001038 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
1039 help='override deps for the specified (comma-separated) '
1040 'platform(s); \'all\' will process all deps_os '
1041 'references')
1042 parser.add_option('-f', '--force', action='store_true', default=True,
1043 help='Deprecated. No effect.')
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001044 (options, args) = parser.parse_args(args)
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001045 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001046 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00001047 raise gclient_utils.Error('client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001048 if options.verbose:
1049 # Print out the .gclient file. This is longer than if we just printed the
1050 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.org116704f2010-06-11 17:34:38 +00001051 print(client.config_content)
maruel@chromium.org5df6a462009-08-28 18:52:26 +00001052 options.force = True
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001053 options.nohooks = False
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001054 return client.RunOnDeps('runhooks', args)
1055
1056
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001057def CMDrevinfo(parser, args):
maruel@chromium.org9eda4112010-06-11 18:56:10 +00001058 """Output revision info mapping for the client and its dependencies.
1059
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00001060 This allows the capture of an overall 'revision' for the source tree that
maruel@chromium.org9eda4112010-06-11 18:56:10 +00001061 can be used to reproduce the same tree in the future. It is only useful for
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00001062 'unpinned dependencies', i.e. DEPS/deps references without a svn revision
1063 number or a git hash. A git branch name isn't 'pinned' since the actual
maruel@chromium.org9eda4112010-06-11 18:56:10 +00001064 commit can change.
1065 """
1066 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
1067 help='override deps for the specified (comma-separated) '
1068 'platform(s); \'all\' will process all deps_os '
1069 'references')
1070 parser.add_option('-s', '--snapshot', action='store_true',
1071 help='creates a snapshot .gclient file of the current '
maruel@chromium.orgdf2b3152010-07-21 17:35:24 +00001072 'version of all repositories to reproduce the tree')
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001073 (options, args) = parser.parse_args(args)
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001074 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001075 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00001076 raise gclient_utils.Error('client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001077 client.PrintRevInfo()
maruel@chromium.org79692d62010-05-14 18:57:13 +00001078 return 0
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001079
1080
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001081def Command(name):
1082 return getattr(sys.modules[__name__], 'CMD' + name, None)
1083
1084
1085def CMDhelp(parser, args):
1086 """Prints list of commands or help for a specific command."""
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001087 (_, args) = parser.parse_args(args)
maruel@chromium.orgddff62d2010-05-17 21:02:36 +00001088 if len(args) == 1:
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001089 return Main(args + ['--help'])
maruel@chromium.orgddff62d2010-05-17 21:02:36 +00001090 parser.print_help()
1091 return 0
1092
1093
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001094def GenUsage(parser, command):
1095 """Modify an OptParse object with the function's documentation."""
1096 obj = Command(command)
1097 if command == 'help':
1098 command = '<command>'
1099 # OptParser.description prefer nicely non-formatted strings.
1100 parser.description = re.sub('[\r\n ]{2,}', ' ', obj.__doc__)
1101 usage = getattr(obj, 'usage', '')
1102 parser.set_usage('%%prog %s [options] %s' % (command, usage))
1103 parser.epilog = getattr(obj, 'epilog', None)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001104
1105
1106def Main(argv):
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001107 """Doesn't parse the arguments here, just find the right subcommand to
1108 execute."""
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001109 try:
1110 # Do it late so all commands are listed.
1111 CMDhelp.usage = ('\n\nCommands are:\n' + '\n'.join([
1112 ' %-10s %s' % (fn[3:], Command(fn[3:]).__doc__.split('\n')[0].strip())
1113 for fn in dir(sys.modules[__name__]) if fn.startswith('CMD')]))
1114 parser = optparse.OptionParser(version='%prog ' + __version__)
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001115 parser.add_option('-v', '--verbose', action='count', default=0,
1116 help='Produces additional output for diagnostics. Can be '
1117 'used up to three times for more logging info.')
1118 parser.add_option('--gclientfile', dest='config_filename',
1119 default=os.environ.get('GCLIENT_FILE', '.gclient'),
1120 help='Specify an alternate %default file')
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001121 # Integrate standard options processing.
1122 old_parser = parser.parse_args
1123 def Parse(args):
1124 (options, args) = old_parser(args)
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001125 level = None
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001126 if options.verbose == 2:
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001127 level = logging.INFO
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001128 elif options.verbose > 2:
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001129 level = logging.DEBUG
1130 logging.basicConfig(level=level,
1131 format='%(module)s(%(lineno)d) %(funcName)s:%(message)s')
1132 options.entries_filename = options.config_filename + '_entries'
maruel@chromium.orge3216c62010-07-08 03:31:43 +00001133
1134 # These hacks need to die.
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001135 if not hasattr(options, 'revisions'):
1136 # GClient.RunOnDeps expects it even if not applicable.
1137 options.revisions = []
1138 if not hasattr(options, 'head'):
1139 options.head = None
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001140 if not hasattr(options, 'nohooks'):
1141 options.nohooks = True
1142 if not hasattr(options, 'deps_os'):
1143 options.deps_os = None
maruel@chromium.orge3216c62010-07-08 03:31:43 +00001144 if not hasattr(options, 'manually_grab_svn_rev'):
1145 options.manually_grab_svn_rev = None
1146 if not hasattr(options, 'force'):
1147 options.force = None
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001148 return (options, args)
1149 parser.parse_args = Parse
1150 # We don't want wordwrapping in epilog (usually examples)
1151 parser.format_epilog = lambda _: parser.epilog or ''
1152 if argv:
1153 command = Command(argv[0])
1154 if command:
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001155 # 'fix' the usage and the description now that we know the subcommand.
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001156 GenUsage(parser, argv[0])
1157 return command(parser, argv[1:])
1158 # Not a known command. Default to help.
1159 GenUsage(parser, 'help')
1160 return CMDhelp(parser, argv)
1161 except gclient_utils.Error, e:
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001162 print >> sys.stderr, 'Error: %s' % str(e)
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001163 return 1
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001164
1165
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001166if '__main__' == __name__:
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001167 sys.exit(Main(sys.argv[1:]))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001168
1169# vim: ts=2:sw=2:tw=80:et: