blob: 5de746bb941eac8240f28894bc5fd7986325646a [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.org0b6a0842010-06-15 14:34:19 +000052__version__ = "0.4.1"
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000053
54import errno
maruel@chromium.org754960e2009-09-21 12:31:05 +000055import logging
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000056import optparse
57import os
msb@chromium.org2e38de72009-09-28 17:04:47 +000058import pprint
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000059import re
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
99 def GetUrl(self, target_name, sub_deps_base_url, root_dir, sub_deps):
100 """Resolve the URL for this From entry."""
101 sub_deps_target_name = target_name
102 if self.sub_target_name:
103 sub_deps_target_name = self.sub_target_name
104 url = sub_deps[sub_deps_target_name]
105 if url.startswith('/'):
106 # If it's a relative URL, we need to resolve the URL relative to the
107 # sub deps base URL.
108 if not isinstance(sub_deps_base_url, basestring):
109 sub_deps_base_url = sub_deps_base_url.GetPath()
110 scm = gclient_scm.CreateSCM(sub_deps_base_url, root_dir,
111 None)
112 url = scm.FullUrlForRelativeUrl(url)
113 return url
114
115 class FileImpl(object):
116 """Used to implement the File('') syntax which lets you sync a single file
117 from an SVN repo."""
118
119 def __init__(self, file_location):
120 self.file_location = file_location
121
122 def __str__(self):
123 return 'File("%s")' % self.file_location
124
125 def GetPath(self):
126 return os.path.split(self.file_location)[0]
127
128 def GetFilename(self):
129 rev_tokens = self.file_location.split('@')
130 return os.path.split(rev_tokens[0])[1]
131
132 def GetRevision(self):
133 rev_tokens = self.file_location.split('@')
134 if len(rev_tokens) > 1:
135 return rev_tokens[1]
136 return None
137
138 class VarImpl(object):
139 def __init__(self, custom_vars, local_scope):
140 self._custom_vars = custom_vars
141 self._local_scope = local_scope
142
143 def Lookup(self, var_name):
144 """Implements the Var syntax."""
145 if var_name in self._custom_vars:
146 return self._custom_vars[var_name]
147 elif var_name in self._local_scope.get("vars", {}):
148 return self._local_scope["vars"][var_name]
149 raise gclient_utils.Error("Var is not defined: %s" % var_name)
150
151
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000152class Dependency(GClientKeywords):
153 """Object that represents a dependency checkout."""
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000154 DEPS_FILE = 'DEPS'
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000155
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000156 def __init__(self, parent, name, url, safesync_url=None, custom_deps=None,
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000157 custom_vars=None, deps_file=None):
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000158 GClientKeywords.__init__(self)
159 self.parent = parent
160 self.name = name
161 self.url = url
162 # These 2 are only set in .gclient and not in DEPS files.
163 self.safesync_url = safesync_url
164 self.custom_vars = custom_vars or {}
165 self.custom_deps = custom_deps or {}
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000166 self.deps_hooks = []
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000167 self.dependencies = []
168 self.deps_file = deps_file or self.DEPS_FILE
maruel@chromium.org03852792010-06-18 16:22:06 +0000169 self.deps_parsed = False
170 self.direct_reference = False
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000171
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000172 # Sanity checks
173 if not self.name and self.parent:
174 raise gclient_utils.Error('Dependency without name')
175 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.org03852792010-06-18 16:22:06 +0000184 def ParseDepsFile(self, direct_reference):
185 """Parses the DEPS file for this dependency."""
186 if direct_reference:
187 # Maybe it was referenced earlier by a From() keyword but it's now
188 # directly referenced.
189 self.direct_reference = direct_reference
190 if self.deps_parsed:
191 return
192 self.deps_parsed = True
193 filepath = os.path.join(self.root_dir(), self.name, self.deps_file)
194 if not os.path.isfile(filepath):
195 return
196 deps_content = gclient_utils.FileRead(filepath)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000197
maruel@chromium.org03852792010-06-18 16:22:06 +0000198 # Eval the content.
199 # One thing is unintuitive, vars= {} must happen before Var() use.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000200 local_scope = {}
maruel@chromium.org03852792010-06-18 16:22:06 +0000201 var = self.VarImpl(self.custom_vars, local_scope)
tony@chromium.org4b5b1772010-04-08 01:52:56 +0000202 global_scope = {
maruel@chromium.org03852792010-06-18 16:22:06 +0000203 'File': self.FileImpl,
204 'From': self.FromImpl,
205 'Var': var.Lookup,
206 'deps_os': {},
tony@chromium.org4b5b1772010-04-08 01:52:56 +0000207 }
maruel@chromium.org03852792010-06-18 16:22:06 +0000208 exec(deps_content, global_scope, local_scope)
209 deps = local_scope.get('deps', {})
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000210 # load os specific dependencies if defined. these dependencies may
211 # override or extend the values defined by the 'deps' member.
maruel@chromium.org03852792010-06-18 16:22:06 +0000212 if 'deps_os' in local_scope:
213 for deps_os_key in self.enforced_os():
214 os_deps = local_scope['deps_os'].get(deps_os_key, {})
215 if len(self.enforced_os()) > 1:
216 # Ignore any conflict when including deps for more than one
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000217 # platform, so we collect the broadest set of dependencies available.
218 # We may end up with the wrong revision of something for our
219 # platform, but this is the best we can do.
220 deps.update([x for x in os_deps.items() if not x[0] in deps])
221 else:
222 deps.update(os_deps)
223
maruel@chromium.org03852792010-06-18 16:22:06 +0000224 self.deps_hooks.extend(local_scope.get('hooks', []))
225
226 # If a line is in custom_deps, but not in the solution, we want to append
227 # this line to the solution.
228 for d in self.custom_deps:
229 if d not in deps:
230 deps[d] = self.custom_deps[d]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000231
232 # If use_relative_paths is set in the DEPS file, regenerate
233 # the dictionary using paths relative to the directory containing
234 # the DEPS file.
maruel@chromium.org03852792010-06-18 16:22:06 +0000235 use_relative_paths = local_scope.get('use_relative_paths', False)
236 if use_relative_paths:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000237 rel_deps = {}
238 for d, url in deps.items():
239 # normpath is required to allow DEPS to use .. in their
240 # dependency local path.
maruel@chromium.org03852792010-06-18 16:22:06 +0000241 rel_deps[os.path.normpath(os.path.join(self.name, d))] = url
242 deps = rel_deps
243 # TODO(maruel): Add these dependencies into self.dependencies.
244 return deps
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000245
maruel@chromium.org03852792010-06-18 16:22:06 +0000246 def _ParseAllDeps(self, solution_urls):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000247 """Parse the complete list of dependencies for the client.
248
249 Args:
250 solution_urls: A dict mapping module names (as relative paths) to URLs
251 corresponding to the solutions specified by the client. This parameter
252 is passed as an optimization.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000253
254 Returns:
255 A dict mapping module names (as relative paths) to URLs corresponding
256 to the entire set of dependencies to checkout for the given client.
257
258 Raises:
259 Error: If a dependency conflicts with another dependency or of a solution.
260 """
261 deps = {}
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000262 for solution in self.dependencies:
maruel@chromium.org03852792010-06-18 16:22:06 +0000263 solution_deps = solution.ParseDepsFile(True)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000264
265 # If a line is in custom_deps, but not in the solution, we want to append
266 # this line to the solution.
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000267 for d in solution.custom_deps:
268 if d not in solution_deps:
269 solution_deps[d] = solution.custom_deps[d]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000270
271 for d in solution_deps:
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000272 if d in solution.custom_deps:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000273 # Dependency is overriden.
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000274 url = solution.custom_deps[d]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000275 if url is None:
276 continue
277 else:
278 url = solution_deps[d]
279 # if we have a From reference dependent on another solution, then
280 # just skip the From reference. When we pull deps for the solution,
281 # we will take care of this dependency.
282 #
283 # If multiple solutions all have the same From reference, then we
284 # should only add one to our list of dependencies.
tony@chromium.org4b5b1772010-04-08 01:52:56 +0000285 if isinstance(url, self.FromImpl):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000286 if url.module_name in solution_urls:
287 # Already parsed.
288 continue
289 if d in deps and type(deps[d]) != str:
290 if url.module_name == deps[d].module_name:
291 continue
tony@chromium.org4b5b1772010-04-08 01:52:56 +0000292 elif isinstance(url, str):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000293 parsed_url = urlparse.urlparse(url)
294 scheme = parsed_url[0]
295 if not scheme:
296 # A relative url. Fetch the real base.
297 path = parsed_url[2]
298 if path[0] != "/":
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000299 raise gclient_utils.Error(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000300 "relative DEPS entry \"%s\" must begin with a slash" % d)
msb@chromium.orge6f78352010-01-13 17:05:33 +0000301 # Create a scm just to query the full url.
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000302 scm = gclient_scm.CreateSCM(solution.url, self.root_dir(),
303 None)
msb@chromium.orge6f78352010-01-13 17:05:33 +0000304 url = scm.FullUrlForRelativeUrl(url)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000305 if d in deps and deps[d] != url:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000306 raise gclient_utils.Error(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000307 "Solutions have conflicting versions of dependency \"%s\"" % d)
308 if d in solution_urls and solution_urls[d] != url:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000309 raise gclient_utils.Error(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000310 "Dependency \"%s\" conflicts with specified solution" % d)
311 # Grab the dependency.
312 deps[d] = url
313 return deps
314
phajdan.jr@chromium.org71b40682009-07-31 23:40:09 +0000315 def _RunHookAction(self, hook_dict, matching_file_list):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000316 """Runs the action from a single hook.
317 """
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000318 logging.info(hook_dict)
319 logging.info(matching_file_list)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000320 command = hook_dict['action'][:]
321 if command[0] == 'python':
322 # If the hook specified "python" as the first item, the action is a
323 # Python script. Run it by starting a new copy of the same
324 # interpreter.
325 command[0] = sys.executable
326
phajdan.jr@chromium.org71b40682009-07-31 23:40:09 +0000327 if '$matching_files' in command:
phajdan.jr@chromium.org68f2e092009-08-06 17:05:35 +0000328 splice_index = command.index('$matching_files')
329 command[splice_index:splice_index + 1] = matching_file_list
phajdan.jr@chromium.org71b40682009-07-31 23:40:09 +0000330
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000331 # Use a discrete exit status code of 2 to indicate that a hook action
332 # failed. Users of this script may wish to treat hook action failures
333 # differently from VC failures.
maruel@chromium.org75a59272010-06-11 22:34:03 +0000334 gclient_utils.SubprocessCall(command, self.root_dir(), fail_status=2)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000335
336 def _RunHooks(self, command, file_list, is_using_git):
337 """Evaluates all hooks, running actions as needed.
338 """
339 # Hooks only run for these command types.
340 if not command in ('update', 'revert', 'runhooks'):
341 return
342
evan@chromium.org67820ef2009-07-27 17:23:00 +0000343 # Hooks only run when --nohooks is not specified
344 if self._options.nohooks:
345 return
346
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000347 # Get any hooks from the .gclient file.
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000348 hooks = self.deps_hooks[:]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000349 # Add any hooks found in DEPS files.
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000350 for d in self.dependencies:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000351 hooks.extend(d.deps_hooks)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000352
353 # If "--force" was specified, run all hooks regardless of what files have
354 # changed. If the user is using git, then we don't know what files have
355 # changed so we always run all hooks.
356 if self._options.force or is_using_git:
357 for hook_dict in hooks:
phajdan.jr@chromium.org71b40682009-07-31 23:40:09 +0000358 self._RunHookAction(hook_dict, [])
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000359 return
360
361 # Run hooks on the basis of whether the files from the gclient operation
362 # match each hook's pattern.
363 for hook_dict in hooks:
364 pattern = re.compile(hook_dict['pattern'])
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000365 matching_file_list = [f for f in file_list if pattern.search(f)]
phajdan.jr@chromium.org71b40682009-07-31 23:40:09 +0000366 if matching_file_list:
367 self._RunHookAction(hook_dict, matching_file_list)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000368
maruel@chromium.org03852792010-06-18 16:22:06 +0000369 def root_dir(self):
370 return self.parent.root_dir()
371
372 def enforced_os(self):
373 return self.parent.enforced_os()
374
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000375
376class GClient(Dependency):
377 """Main gclient checkout root where .gclient resides."""
378 SUPPORTED_COMMANDS = [
379 'cleanup', 'diff', 'export', 'pack', 'revert', 'status', 'update',
380 'runhooks'
381 ]
382
383 DEPS_OS_CHOICES = {
384 "win32": "win",
385 "win": "win",
386 "cygwin": "win",
387 "darwin": "mac",
388 "mac": "mac",
389 "unix": "unix",
390 "linux": "unix",
391 "linux2": "unix",
392 }
393
394 DEFAULT_CLIENT_FILE_TEXT = ("""\
395solutions = [
396 { "name" : "%(solution_name)s",
397 "url" : "%(solution_url)s",
398 "custom_deps" : {
399 },
400 "safesync_url": "%(safesync_url)s"
401 },
402]
403""")
404
405 DEFAULT_SNAPSHOT_SOLUTION_TEXT = ("""\
406 { "name" : "%(solution_name)s",
407 "url" : "%(solution_url)s",
408 "custom_deps" : {
409 %(solution_deps)s,
410 },
411 "safesync_url": "%(safesync_url)s"
412 },
413""")
414
415 DEFAULT_SNAPSHOT_FILE_TEXT = ("""\
416# Snapshot generated with gclient revinfo --snapshot
417solutions = [
418%(solution_list)s
419]
420""")
421
422 def __init__(self, root_dir, options):
423 Dependency.__init__(self, None, None, None)
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000424 self._options = options
maruel@chromium.org03852792010-06-18 16:22:06 +0000425 if options.deps_os:
426 enforced_os = options.deps_os.split(',')
427 else:
428 enforced_os = [self.DEPS_OS_CHOICES.get(sys.platform, 'unix')]
429 if 'all' in enforced_os:
430 enforced_os = self.DEPS_OS_CHOICES.itervalues()
431 self._enforced_os = list(set(enforced_os))
432 self._root_dir = root_dir
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000433 self.config_content = None
434
435 def SetConfig(self, content):
436 assert self.dependencies == []
437 config_dict = {}
438 self.config_content = content
439 try:
440 exec(content, config_dict)
441 except SyntaxError, e:
442 try:
443 # Try to construct a human readable error message
444 error_message = [
445 'There is a syntax error in your configuration file.',
446 'Line #%s, character %s:' % (e.lineno, e.offset),
447 '"%s"' % re.sub(r'[\r\n]*$', '', e.text) ]
448 except:
449 # Something went wrong, re-raise the original exception
450 raise e
451 else:
452 # Raise a new exception with the human readable message:
453 raise gclient_utils.Error('\n'.join(error_message))
454 for s in config_dict.get('solutions', []):
455 self.dependencies.append(Dependency(
456 self, s['name'], s['url'],
457 s.get('safesync_url', None),
458 s.get('custom_deps', {}),
459 s.get('custom_vars', {})))
460 # .gclient can have hooks.
461 self.deps_hooks = config_dict.get('hooks', [])
462
463 def SaveConfig(self):
464 gclient_utils.FileWrite(os.path.join(self.root_dir(),
465 self._options.config_filename),
466 self.config_content)
467
468 @staticmethod
469 def LoadCurrentConfig(options):
470 """Searches for and loads a .gclient file relative to the current working
471 dir. Returns a GClient object."""
472 path = gclient_utils.FindGclientRoot(os.getcwd(), options.config_filename)
473 if not path:
474 return None
475 client = GClient(path, options)
476 client.SetConfig(gclient_utils.FileRead(
477 os.path.join(path, options.config_filename)))
478 return client
479
480 def SetDefaultConfig(self, solution_name, solution_url, safesync_url):
481 self.SetConfig(self.DEFAULT_CLIENT_FILE_TEXT % {
482 'solution_name': solution_name,
483 'solution_url': solution_url,
484 'safesync_url' : safesync_url,
485 })
486
487 def _SaveEntries(self, entries):
488 """Creates a .gclient_entries file to record the list of unique checkouts.
489
490 The .gclient_entries file lives in the same directory as .gclient.
491
492 Args:
493 entries: A sequence of solution names.
494 """
495 # Sometimes pprint.pformat will use {', sometimes it'll use { ' ... It
496 # makes testing a bit too fun.
497 result = pprint.pformat(entries, 2)
498 if result.startswith('{\''):
499 result = '{ \'' + result[2:]
500 text = "entries = \\\n" + result + '\n'
501 file_path = os.path.join(self.root_dir(), self._options.entries_filename)
502 gclient_utils.FileWrite(file_path, text)
503
504 def _ReadEntries(self):
505 """Read the .gclient_entries file for the given client.
506
507 Returns:
508 A sequence of solution names, which will be empty if there is the
509 entries file hasn't been created yet.
510 """
511 scope = {}
512 filename = os.path.join(self.root_dir(), self._options.entries_filename)
513 if not os.path.exists(filename):
514 return []
515 exec(gclient_utils.FileRead(filename), scope)
516 return scope['entries']
517
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000518 def _EnforceRevisions(self):
maruel@chromium.org918a9ae2010-05-28 15:50:30 +0000519 """Checks for revision overrides."""
520 revision_overrides = {}
maruel@chromium.org307d1792010-05-31 20:03:13 +0000521 if self._options.head:
522 return revision_overrides
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000523 for s in self.dependencies:
524 if not s.safesync_url:
maruel@chromium.org307d1792010-05-31 20:03:13 +0000525 continue
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000526 handle = urllib.urlopen(s.safesync_url)
maruel@chromium.org307d1792010-05-31 20:03:13 +0000527 rev = handle.read().strip()
528 handle.close()
529 if len(rev):
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000530 self._options.revisions.append('%s@%s' % (s.name, rev))
maruel@chromium.org307d1792010-05-31 20:03:13 +0000531 if not self._options.revisions:
532 return revision_overrides
533 # --revision will take over safesync_url.
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000534 solutions_names = [s.name for s in self.dependencies]
maruel@chromium.org307d1792010-05-31 20:03:13 +0000535 index = 0
536 for revision in self._options.revisions:
537 if not '@' in revision:
538 # Support for --revision 123
539 revision = '%s@%s' % (solutions_names[index], revision)
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000540 sol, rev = revision.split('@', 1)
maruel@chromium.org307d1792010-05-31 20:03:13 +0000541 if not sol in solutions_names:
542 #raise gclient_utils.Error('%s is not a valid solution.' % sol)
543 print >> sys.stderr, ('Please fix your script, having invalid '
544 '--revision flags will soon considered an error.')
545 else:
maruel@chromium.org918a9ae2010-05-28 15:50:30 +0000546 revision_overrides[sol] = rev
maruel@chromium.org307d1792010-05-31 20:03:13 +0000547 index += 1
maruel@chromium.org918a9ae2010-05-28 15:50:30 +0000548 return revision_overrides
549
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000550 def RunOnDeps(self, command, args):
551 """Runs a command on each dependency in a client and its dependencies.
552
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000553 Args:
554 command: The command to use (e.g., 'status' or 'diff')
555 args: list of str - extra arguments to add to the command line.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000556 """
maruel@chromium.org116704f2010-06-11 17:34:38 +0000557 if not command in self.SUPPORTED_COMMANDS:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000558 raise gclient_utils.Error("'%s' is an unsupported command" % command)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000559
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000560 if not self.dependencies:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000561 raise gclient_utils.Error("No solution specified")
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000562 revision_overrides = self._EnforceRevisions()
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000563
564 # When running runhooks --force, there's no need to consult the SCM.
565 # All known hooks are expected to run unconditionally regardless of working
566 # copy state, so skip the SCM status check.
567 run_scm = not (command == 'runhooks' and self._options.force)
568
569 entries = {}
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000570 file_list = []
571 # Run on the base solutions first.
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000572 for solution in self.dependencies:
573 name = solution.name
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000574 if name in entries:
575 raise gclient_utils.Error("solution %s specified more than once" % name)
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000576 url = solution.url
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000577 entries[name] = url
578 if run_scm and url:
579 self._options.revision = revision_overrides.get(name)
maruel@chromium.org75a59272010-06-11 22:34:03 +0000580 scm = gclient_scm.CreateSCM(url, self.root_dir(), name)
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000581 scm.RunCommand(command, self._options, args, file_list)
582 file_list = [os.path.join(name, f.strip()) for f in file_list]
583 self._options.revision = None
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000584
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000585 # Process the dependencies next (sort alphanumerically to ensure that
586 # containing directories get populated first and for readability)
maruel@chromium.org03852792010-06-18 16:22:06 +0000587 deps = self._ParseAllDeps(entries)
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000588 deps_to_process = deps.keys()
589 deps_to_process.sort()
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000590
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000591 # First pass for direct dependencies.
592 if command == 'update' and not self._options.verbose:
593 pm = Progress('Syncing projects', len(deps_to_process))
594 for d in deps_to_process:
nasser@codeaurora.org1f7a3d12010-02-04 15:11:50 +0000595 if command == 'update' and not self._options.verbose:
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000596 pm.update()
597 if type(deps[d]) == str:
598 url = deps[d]
599 entries[d] = url
600 if run_scm:
601 self._options.revision = revision_overrides.get(d)
maruel@chromium.org75a59272010-06-11 22:34:03 +0000602 scm = gclient_scm.CreateSCM(url, self.root_dir(), d)
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000603 scm.RunCommand(command, self._options, args, file_list)
604 self._options.revision = None
605 elif isinstance(deps[d], self.FileImpl):
maruel@chromium.org491c04b2010-05-17 18:17:44 +0000606 file_dep = deps[d]
607 self._options.revision = file_dep.GetRevision()
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000608 if run_scm:
maruel@chromium.org75a59272010-06-11 22:34:03 +0000609 scm = gclient_scm.CreateSCM(file_dep.GetPath(), self.root_dir(), d)
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000610 scm.RunCommand("updatesingle", self._options,
maruel@chromium.org491c04b2010-05-17 18:17:44 +0000611 args + [file_dep.GetFilename()], file_list)
maruel@chromium.org79692d62010-05-14 18:57:13 +0000612
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000613 if command == 'update' and not self._options.verbose:
614 pm.end()
piman@chromium.org6f363722010-04-27 00:41:09 +0000615
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000616 # Second pass for inherited deps (via the From keyword)
617 for d in deps_to_process:
618 if isinstance(deps[d], self.FromImpl):
maruel@chromium.org75a59272010-06-11 22:34:03 +0000619 filename = os.path.join(self.root_dir(),
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000620 deps[d].module_name,
maruel@chromium.org1f7d1182010-05-17 18:17:38 +0000621 self.DEPS_FILE)
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000622 content = gclient_utils.FileRead(filename)
623 sub_deps = self._ParseSolutionDeps(deps[d].module_name, content, {},
624 False)
625 # Getting the URL from the sub_deps file can involve having to resolve
626 # a File() or having to resolve a relative URL. To resolve relative
627 # URLs, we need to pass in the orignal sub deps URL.
628 sub_deps_base_url = deps[deps[d].module_name]
maruel@chromium.org75a59272010-06-11 22:34:03 +0000629 url = deps[d].GetUrl(d, sub_deps_base_url, self.root_dir(), sub_deps)
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000630 entries[d] = url
631 if run_scm:
632 self._options.revision = revision_overrides.get(d)
maruel@chromium.org75a59272010-06-11 22:34:03 +0000633 scm = gclient_scm.CreateSCM(url, self.root_dir(), d)
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000634 scm.RunCommand(command, self._options, args, file_list)
635 self._options.revision = None
gspencer@google.comdf2d5902009-09-11 22:16:21 +0000636
phajdan.jr@chromium.orgd83b2b22009-08-11 15:30:55 +0000637 # Convert all absolute paths to relative.
638 for i in range(len(file_list)):
639 # TODO(phajdan.jr): We should know exactly when the paths are absolute.
640 # It depends on the command being executed (like runhooks vs sync).
641 if not os.path.isabs(file_list[i]):
642 continue
643
maruel@chromium.org75a59272010-06-11 22:34:03 +0000644 prefix = os.path.commonprefix([self.root_dir().lower(),
phajdan.jr@chromium.orgd83b2b22009-08-11 15:30:55 +0000645 file_list[i].lower()])
646 file_list[i] = file_list[i][len(prefix):]
647
648 # Strip any leading path separators.
649 while file_list[i].startswith('\\') or file_list[i].startswith('/'):
650 file_list[i] = file_list[i][1:]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000651
maruel@chromium.org75a59272010-06-11 22:34:03 +0000652 is_using_git = gclient_utils.IsUsingGit(self.root_dir(), entries.keys())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000653 self._RunHooks(command, file_list, is_using_git)
654
655 if command == 'update':
ajwong@chromium.orgcdcee802009-06-23 15:30:42 +0000656 # Notify the user if there is an orphaned entry in their working copy.
657 # Only delete the directory if there are no changes in it, and
658 # delete_unversioned_trees is set to true.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000659 prev_entries = self._ReadEntries()
660 for entry in prev_entries:
maruel@chromium.orgc5e9aec2009-08-03 18:25:56 +0000661 # Fix path separator on Windows.
662 entry_fixed = entry.replace('/', os.path.sep)
maruel@chromium.org75a59272010-06-11 22:34:03 +0000663 e_dir = os.path.join(self.root_dir(), entry_fixed)
maruel@chromium.orgc5e9aec2009-08-03 18:25:56 +0000664 # Use entry and not entry_fixed there.
maruel@chromium.org0329e672009-05-13 18:41:04 +0000665 if entry not in entries and os.path.exists(e_dir):
msb@chromium.org83017012009-09-28 18:52:12 +0000666 modified_files = False
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000667 if isinstance(prev_entries, list):
msb@chromium.org83017012009-09-28 18:52:12 +0000668 # old .gclient_entries format was list, now dict
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000669 modified_files = gclient_scm.scm.SVN.CaptureStatus(e_dir)
msb@chromium.org83017012009-09-28 18:52:12 +0000670 else:
671 file_list = []
maruel@chromium.org75a59272010-06-11 22:34:03 +0000672 scm = gclient_scm.CreateSCM(prev_entries[entry], self.root_dir(),
msb@chromium.org83017012009-09-28 18:52:12 +0000673 entry_fixed)
674 scm.status(self._options, [], file_list)
675 modified_files = file_list != []
676 if not self._options.delete_unversioned_trees or modified_files:
maruel@chromium.orgc5e9aec2009-08-03 18:25:56 +0000677 # There are modified files in this entry. Keep warning until
678 # removed.
maruel@chromium.orgc5e9aec2009-08-03 18:25:56 +0000679 print(("\nWARNING: \"%s\" is no longer part of this client. "
680 "It is recommended that you manually remove it.\n") %
681 entry_fixed)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000682 else:
683 # Delete the entry
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000684 print("\n________ deleting \'%s\' " +
maruel@chromium.org75a59272010-06-11 22:34:03 +0000685 "in \'%s\'") % (entry_fixed, self.root_dir())
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000686 gclient_utils.RemoveDirectory(e_dir)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000687 # record the current list of entries for next time
688 self._SaveEntries(entries)
maruel@chromium.org17cdf762010-05-28 17:30:52 +0000689 return 0
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000690
691 def PrintRevInfo(self):
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000692 """Output revision info mapping for the client and its dependencies.
693
694 This allows the capture of an overall "revision" for the source tree that
maruel@chromium.org918a9ae2010-05-28 15:50:30 +0000695 can be used to reproduce the same tree in the future. It is only useful for
696 "unpinned dependencies", i.e. DEPS/deps references without a svn revision
697 number or a git hash. A git branch name isn't "pinned" since the actual
698 commit can change.
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000699
700 The --snapshot option allows creating a .gclient file to reproduce the tree.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000701 """
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000702 if not self.dependencies:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000703 raise gclient_utils.Error("No solution specified")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000704
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000705 # Inner helper to generate base url and rev tuple
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000706 def GetURLAndRev(name, original_url):
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000707 url, _ = gclient_utils.SplitUrlRevision(original_url)
maruel@chromium.org75a59272010-06-11 22:34:03 +0000708 scm = gclient_scm.CreateSCM(original_url, self.root_dir(), name)
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000709 return (url, scm.revinfo(self._options, [], None))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000710
maruel@chromium.orge3da35f2010-03-09 21:40:45 +0000711 # text of the snapshot gclient file
712 new_gclient = ""
713 # Dictionary of { path : SCM url } to ensure no duplicate solutions
714 solution_names = {}
nasser@codeaurora.orgde8f3522010-03-11 23:47:44 +0000715 entries = {}
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000716 # Run on the base solutions first.
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000717 for solution in self.dependencies:
maruel@chromium.orge3da35f2010-03-09 21:40:45 +0000718 # Dictionary of { path : SCM url } to describe the gclient checkout
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000719 name = solution.name
maruel@chromium.orge3da35f2010-03-09 21:40:45 +0000720 if name in solution_names:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000721 raise gclient_utils.Error("solution %s specified more than once" % name)
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000722 (url, rev) = GetURLAndRev(name, solution.url)
msb@chromium.org770ff9e2009-09-23 17:18:18 +0000723 entries[name] = "%s@%s" % (url, rev)
maruel@chromium.orge3da35f2010-03-09 21:40:45 +0000724 solution_names[name] = "%s@%s" % (url, rev)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000725
nasser@codeaurora.orgde8f3522010-03-11 23:47:44 +0000726 # Process the dependencies next (sort alphanumerically to ensure that
727 # containing directories get populated first and for readability)
maruel@chromium.org03852792010-06-18 16:22:06 +0000728 deps = self._ParseAllDeps(entries)
nasser@codeaurora.orgde8f3522010-03-11 23:47:44 +0000729 deps_to_process = deps.keys()
730 deps_to_process.sort()
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000731
nasser@codeaurora.orgde8f3522010-03-11 23:47:44 +0000732 # First pass for direct dependencies.
733 for d in deps_to_process:
734 if type(deps[d]) == str:
735 (url, rev) = GetURLAndRev(d, deps[d])
736 entries[d] = "%s@%s" % (url, rev)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000737
nasser@codeaurora.orgde8f3522010-03-11 23:47:44 +0000738 # Second pass for inherited deps (via the From keyword)
739 for d in deps_to_process:
tony@chromium.org4b5b1772010-04-08 01:52:56 +0000740 if isinstance(deps[d], self.FromImpl):
nasser@codeaurora.orgde8f3522010-03-11 23:47:44 +0000741 deps_parent_url = entries[deps[d].module_name]
742 if deps_parent_url.find("@") < 0:
743 raise gclient_utils.Error("From %s missing revisioned url" %
744 deps[d].module_name)
745 content = gclient_utils.FileRead(os.path.join(
maruel@chromium.org75a59272010-06-11 22:34:03 +0000746 self.root_dir(),
nasser@codeaurora.orgde8f3522010-03-11 23:47:44 +0000747 deps[d].module_name,
maruel@chromium.org491c04b2010-05-17 18:17:44 +0000748 self.DEPS_FILE))
749 sub_deps = self._ParseSolutionDeps(deps[d].module_name, content, {},
750 False)
nasser@codeaurora.orgde8f3522010-03-11 23:47:44 +0000751 (url, rev) = GetURLAndRev(d, sub_deps[d])
752 entries[d] = "%s@%s" % (url, rev)
maruel@chromium.orge3da35f2010-03-09 21:40:45 +0000753
nasser@codeaurora.orgde8f3522010-03-11 23:47:44 +0000754 # Build the snapshot configuration string
755 if self._options.snapshot:
756 url = entries.pop(name)
757 custom_deps = ",\n ".join(["\"%s\": \"%s\"" % (x, entries[x])
758 for x in sorted(entries.keys())])
maruel@chromium.orge3da35f2010-03-09 21:40:45 +0000759
maruel@chromium.org1f7d1182010-05-17 18:17:38 +0000760 new_gclient += self.DEFAULT_SNAPSHOT_SOLUTION_TEXT % {
nasser@codeaurora.orgde8f3522010-03-11 23:47:44 +0000761 'solution_name': name,
762 'solution_url': url,
763 'safesync_url' : "",
764 'solution_deps': custom_deps,
765 }
766 else:
767 print(";\n".join(["%s: %s" % (x, entries[x])
768 for x in sorted(entries.keys())]))
maruel@chromium.orge3da35f2010-03-09 21:40:45 +0000769
770 # Print the snapshot configuration file
771 if self._options.snapshot:
maruel@chromium.org491c04b2010-05-17 18:17:44 +0000772 config = self.DEFAULT_SNAPSHOT_FILE_TEXT % {'solution_list': new_gclient}
maruel@chromium.org75a59272010-06-11 22:34:03 +0000773 snapclient = GClient(self.root_dir(), self._options)
maruel@chromium.orge3da35f2010-03-09 21:40:45 +0000774 snapclient.SetConfig(config)
maruel@chromium.org116704f2010-06-11 17:34:38 +0000775 print(snapclient.config_content)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000776
maruel@chromium.org75a59272010-06-11 22:34:03 +0000777 def root_dir(self):
778 return self._root_dir
779
maruel@chromium.org03852792010-06-18 16:22:06 +0000780 def enforced_os(self):
781 return self._enforced_os
782
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
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000807@attr('usage', '[url] [safesync url]')
808def CMDconfig(parser, args):
maruel@chromium.orgddff62d2010-05-17 21:02:36 +0000809 """Create a .gclient file in the current directory.
810
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000811This specifies the configuration for further commands. After update/sync,
maruel@chromium.org79692d62010-05-14 18:57:13 +0000812top-level DEPS files in each module are read to determine dependent
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000813modules to operate on as well. If optional [url] parameter is
maruel@chromium.org79692d62010-05-14 18:57:13 +0000814provided, then configuration is read from a specified Subversion server
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000815URL.
maruel@chromium.org79692d62010-05-14 18:57:13 +0000816"""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000817 parser.add_option('--spec',
818 help='create a gclient file containing the provided '
819 'string. Due to Cygwin/Python brokenness, it '
820 'probably can\'t contain any newlines.')
821 parser.add_option('--name',
822 help='overrides the default name for the solution')
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000823 (options, args) = parser.parse_args(args)
maruel@chromium.org5fc2a332010-05-26 19:37:15 +0000824 if ((options.spec and args) or len(args) > 2 or
825 (not options.spec and not args)):
826 parser.error('Inconsistent arguments. Use either --spec or one or 2 args')
827
maruel@chromium.org0329e672009-05-13 18:41:04 +0000828 if os.path.exists(options.config_filename):
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000829 raise gclient_utils.Error('%s file already exists in the current directory'
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000830 % options.config_filename)
maruel@chromium.org2806acc2009-05-15 12:33:34 +0000831 client = GClient('.', options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000832 if options.spec:
833 client.SetConfig(options.spec)
834 else:
maruel@chromium.org1ab7ffc2009-06-03 17:21:37 +0000835 base_url = args[0].rstrip('/')
iposva@chromium.org8cf7a392010-04-07 17:20:26 +0000836 if not options.name:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000837 name = base_url.split('/')[-1]
iposva@chromium.org8cf7a392010-04-07 17:20:26 +0000838 else:
839 # specify an alternate relpath for the given URL.
840 name = options.name
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000841 safesync_url = ''
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000842 if len(args) > 1:
843 safesync_url = args[1]
844 client.SetDefaultConfig(name, base_url, safesync_url)
845 client.SaveConfig()
maruel@chromium.org79692d62010-05-14 18:57:13 +0000846 return 0
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000847
848
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000849def CMDexport(parser, args):
maruel@chromium.orgddff62d2010-05-17 21:02:36 +0000850 """Wrapper for svn export for all managed directories."""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000851 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
852 help='override deps for the specified (comma-separated) '
853 'platform(s); \'all\' will process all deps_os '
854 'references')
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000855 (options, args) = parser.parse_args(args)
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +0000856 if len(args) != 1:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000857 raise gclient_utils.Error('Need directory name')
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +0000858 client = GClient.LoadCurrentConfig(options)
859
860 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000861 raise gclient_utils.Error('client not configured; see \'gclient config\'')
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +0000862
863 if options.verbose:
864 # Print out the .gclient file. This is longer than if we just printed the
865 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.org116704f2010-06-11 17:34:38 +0000866 print(client.config_content)
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +0000867 return client.RunOnDeps('export', args)
868
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000869
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000870@attr('epilog', """Example:
871 gclient pack > patch.txt
872 generate simple patch for configured client and dependences
873""")
874def CMDpack(parser, args):
maruel@chromium.org79692d62010-05-14 18:57:13 +0000875 """Generate a patch which can be applied at the root of the tree.
maruel@chromium.orgddff62d2010-05-17 21:02:36 +0000876
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000877Internally, runs 'svn diff'/'git diff' on each checked out module and
maruel@chromium.org79692d62010-05-14 18:57:13 +0000878dependencies, and performs minimal postprocessing of the output. The
879resulting patch is printed to stdout and can be applied to a freshly
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000880checked out tree via 'patch -p0 < patchfile'.
maruel@chromium.org79692d62010-05-14 18:57:13 +0000881"""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000882 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
883 help='override deps for the specified (comma-separated) '
884 'platform(s); \'all\' will process all deps_os '
885 'references')
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000886 (options, args) = parser.parse_args(args)
kbr@google.comab318592009-09-04 00:54:55 +0000887 client = GClient.LoadCurrentConfig(options)
888 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000889 raise gclient_utils.Error('client not configured; see \'gclient config\'')
kbr@google.comab318592009-09-04 00:54:55 +0000890 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)
kbr@google.comab318592009-09-04 00:54:55 +0000894 return client.RunOnDeps('pack', args)
895
896
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000897def CMDstatus(parser, args):
898 """Show modification status for every dependencies."""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000899 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
900 help='override deps for the specified (comma-separated) '
901 'platform(s); \'all\' will process all deps_os '
902 'references')
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000903 (options, args) = parser.parse_args(args)
maruel@chromium.org2806acc2009-05-15 12:33:34 +0000904 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000905 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000906 raise gclient_utils.Error('client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000907 if options.verbose:
908 # Print out the .gclient file. This is longer than if we just printed the
909 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.org116704f2010-06-11 17:34:38 +0000910 print(client.config_content)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000911 return client.RunOnDeps('status', args)
912
913
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000914@attr('epilog', """Examples:
maruel@chromium.org79692d62010-05-14 18:57:13 +0000915 gclient sync
916 update files from SCM according to current configuration,
917 *for modules which have changed since last update or sync*
918 gclient sync --force
919 update files from SCM according to current configuration, for
920 all modules (useful for recovering files deleted from local copy)
921 gclient sync --revision src@31000
922 update src directory to r31000
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000923""")
924def CMDsync(parser, args):
925 """Checkout/update all modules."""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000926 parser.add_option('-f', '--force', action='store_true',
927 help='force update even for unchanged modules')
928 parser.add_option('-n', '--nohooks', action='store_true',
929 help='don\'t run hooks after the update is complete')
930 parser.add_option('-r', '--revision', action='append',
931 dest='revisions', metavar='REV', default=[],
932 help='Enforces revision/hash for the solutions with the '
933 'format src@rev. The src@ part is optional and can be '
934 'skipped. -r can be used multiple times when .gclient '
935 'has multiple solutions configured and will work even '
936 'if the src@ part is skipped.')
937 parser.add_option('-H', '--head', action='store_true',
938 help='skips any safesync_urls specified in '
939 'configured solutions and sync to head instead')
940 parser.add_option('-D', '--delete_unversioned_trees', action='store_true',
941 help='delete any unexpected unversioned trees '
942 'that are in the checkout')
943 parser.add_option('-R', '--reset', action='store_true',
944 help='resets any local changes before updating (git only)')
945 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
946 help='override deps for the specified (comma-separated) '
947 'platform(s); \'all\' will process all deps_os '
948 'references')
949 parser.add_option('-m', '--manually_grab_svn_rev', action='store_true',
950 help='Skip svn up whenever possible by requesting '
951 'actual HEAD revision from the repository')
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000952 (options, args) = parser.parse_args(args)
maruel@chromium.org2806acc2009-05-15 12:33:34 +0000953 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000954
955 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000956 raise gclient_utils.Error('client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000957
maruel@chromium.org307d1792010-05-31 20:03:13 +0000958 if options.revisions and options.head:
959 # TODO(maruel): Make it a parser.error if it doesn't break any builder.
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000960 print('Warning: you cannot use both --head and --revision')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000961
962 if options.verbose:
963 # Print out the .gclient file. This is longer than if we just printed the
964 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.org116704f2010-06-11 17:34:38 +0000965 print(client.config_content)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000966 return client.RunOnDeps('update', args)
967
968
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000969def CMDupdate(parser, args):
maruel@chromium.orgddff62d2010-05-17 21:02:36 +0000970 """Alias for the sync command. Deprecated."""
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000971 return CMDsync(parser, args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000972
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000973def CMDdiff(parser, args):
974 """Displays local diff for every dependencies."""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000975 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
976 help='override deps for the specified (comma-separated) '
977 'platform(s); \'all\' will process all deps_os '
978 'references')
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 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000982 raise gclient_utils.Error('client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000983 if options.verbose:
984 # Print out the .gclient file. This is longer than if we just printed the
985 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.org116704f2010-06-11 17:34:38 +0000986 print(client.config_content)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000987 return client.RunOnDeps('diff', args)
988
989
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000990def CMDrevert(parser, args):
991 """Revert all modifications in every dependencies."""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000992 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
993 help='override deps for the specified (comma-separated) '
994 'platform(s); \'all\' will process all deps_os '
995 'references')
996 parser.add_option('-n', '--nohooks', action='store_true',
997 help='don\'t run hooks after the revert is complete')
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000998 (options, args) = parser.parse_args(args)
999 # --force is implied.
1000 options.force = True
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001001 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001002 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00001003 raise gclient_utils.Error('client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001004 return client.RunOnDeps('revert', args)
1005
1006
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001007def CMDrunhooks(parser, args):
1008 """Runs hooks for files that have been modified in the local working copy."""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00001009 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
1010 help='override deps for the specified (comma-separated) '
1011 'platform(s); \'all\' will process all deps_os '
1012 'references')
1013 parser.add_option('-f', '--force', action='store_true', default=True,
1014 help='Deprecated. No effect.')
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001015 (options, args) = parser.parse_args(args)
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001016 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001017 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00001018 raise gclient_utils.Error('client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001019 if options.verbose:
1020 # Print out the .gclient file. This is longer than if we just printed the
1021 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.org116704f2010-06-11 17:34:38 +00001022 print(client.config_content)
maruel@chromium.org5df6a462009-08-28 18:52:26 +00001023 options.force = True
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001024 options.nohooks = False
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001025 return client.RunOnDeps('runhooks', args)
1026
1027
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001028def CMDrevinfo(parser, args):
maruel@chromium.org9eda4112010-06-11 18:56:10 +00001029 """Output revision info mapping for the client and its dependencies.
1030
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00001031 This allows the capture of an overall 'revision' for the source tree that
maruel@chromium.org9eda4112010-06-11 18:56:10 +00001032 can be used to reproduce the same tree in the future. It is only useful for
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00001033 'unpinned dependencies', i.e. DEPS/deps references without a svn revision
1034 number or a git hash. A git branch name isn't 'pinned' since the actual
maruel@chromium.org9eda4112010-06-11 18:56:10 +00001035 commit can change.
1036 """
1037 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
1038 help='override deps for the specified (comma-separated) '
1039 'platform(s); \'all\' will process all deps_os '
1040 'references')
1041 parser.add_option('-s', '--snapshot', action='store_true',
1042 help='creates a snapshot .gclient file of the current '
1043 'version of all repositories to reproduce the tree, '
1044 'implies -a')
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001045 (options, args) = parser.parse_args(args)
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001046 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001047 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00001048 raise gclient_utils.Error('client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001049 client.PrintRevInfo()
maruel@chromium.org79692d62010-05-14 18:57:13 +00001050 return 0
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001051
1052
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001053def Command(name):
1054 return getattr(sys.modules[__name__], 'CMD' + name, None)
1055
1056
1057def CMDhelp(parser, args):
1058 """Prints list of commands or help for a specific command."""
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001059 (_, args) = parser.parse_args(args)
maruel@chromium.orgddff62d2010-05-17 21:02:36 +00001060 if len(args) == 1:
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001061 return Main(args + ['--help'])
maruel@chromium.orgddff62d2010-05-17 21:02:36 +00001062 parser.print_help()
1063 return 0
1064
1065
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001066def GenUsage(parser, command):
1067 """Modify an OptParse object with the function's documentation."""
1068 obj = Command(command)
1069 if command == 'help':
1070 command = '<command>'
1071 # OptParser.description prefer nicely non-formatted strings.
1072 parser.description = re.sub('[\r\n ]{2,}', ' ', obj.__doc__)
1073 usage = getattr(obj, 'usage', '')
1074 parser.set_usage('%%prog %s [options] %s' % (command, usage))
1075 parser.epilog = getattr(obj, 'epilog', None)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001076
1077
1078def Main(argv):
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001079 """Doesn't parse the arguments here, just find the right subcommand to
1080 execute."""
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001081 try:
1082 # Do it late so all commands are listed.
1083 CMDhelp.usage = ('\n\nCommands are:\n' + '\n'.join([
1084 ' %-10s %s' % (fn[3:], Command(fn[3:]).__doc__.split('\n')[0].strip())
1085 for fn in dir(sys.modules[__name__]) if fn.startswith('CMD')]))
1086 parser = optparse.OptionParser(version='%prog ' + __version__)
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001087 parser.add_option('-v', '--verbose', action='count', default=0,
1088 help='Produces additional output for diagnostics. Can be '
1089 'used up to three times for more logging info.')
1090 parser.add_option('--gclientfile', dest='config_filename',
1091 default=os.environ.get('GCLIENT_FILE', '.gclient'),
1092 help='Specify an alternate %default file')
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001093 # Integrate standard options processing.
1094 old_parser = parser.parse_args
1095 def Parse(args):
1096 (options, args) = old_parser(args)
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001097 level = None
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001098 if options.verbose == 2:
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001099 level = logging.INFO
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001100 elif options.verbose > 2:
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001101 level = logging.DEBUG
1102 logging.basicConfig(level=level,
1103 format='%(module)s(%(lineno)d) %(funcName)s:%(message)s')
1104 options.entries_filename = options.config_filename + '_entries'
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001105 if not hasattr(options, 'revisions'):
1106 # GClient.RunOnDeps expects it even if not applicable.
1107 options.revisions = []
1108 if not hasattr(options, 'head'):
1109 options.head = None
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001110 if not hasattr(options, 'nohooks'):
1111 options.nohooks = True
1112 if not hasattr(options, 'deps_os'):
1113 options.deps_os = None
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001114 return (options, args)
1115 parser.parse_args = Parse
1116 # We don't want wordwrapping in epilog (usually examples)
1117 parser.format_epilog = lambda _: parser.epilog or ''
1118 if argv:
1119 command = Command(argv[0])
1120 if command:
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001121 # 'fix' the usage and the description now that we know the subcommand.
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001122 GenUsage(parser, argv[0])
1123 return command(parser, argv[1:])
1124 # Not a known command. Default to help.
1125 GenUsage(parser, 'help')
1126 return CMDhelp(parser, argv)
1127 except gclient_utils.Error, e:
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001128 print >> sys.stderr, 'Error: %s' % str(e)
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001129 return 1
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001130
1131
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001132if '__main__' == __name__:
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001133 sys.exit(Main(sys.argv[1:]))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001134
1135# vim: ts=2:sw=2:tw=80:et: