blob: e8eab325cc7c9d68aef3a07a6ac862128aeee8d6 [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.org6b570492010-06-21 18:51:58 +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.org6b570492010-06-21 18:51:58 +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.org6b570492010-06-21 18:51:58 +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.org6b570492010-06-21 18:51:58 +0000201 var = self.VarImpl(self.custom_vars, local_scope)
tony@chromium.org4b5b1772010-04-08 01:52:56 +0000202 global_scope = {
maruel@chromium.org6b570492010-06-21 18:51:58 +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.org6b570492010-06-21 18:51:58 +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.org6b570492010-06-21 18:51:58 +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.org6b570492010-06-21 18:51:58 +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.org6b570492010-06-21 18:51:58 +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.org6b570492010-06-21 18:51:58 +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.org6b570492010-06-21 18:51:58 +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.org6b570492010-06-21 18:51:58 +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.org6b570492010-06-21 18:51:58 +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.orgba0fce72010-06-18 16:47:48 +0000424 self._options = options
maruel@chromium.org6b570492010-06-21 18:51:58 +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.org6b570492010-06-21 18:51:58 +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):
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000619 # Getting the URL from the sub_deps file can involve having to resolve
620 # a File() or having to resolve a relative URL. To resolve relative
621 # URLs, we need to pass in the orignal sub deps URL.
622 sub_deps_base_url = deps[deps[d].module_name]
maruel@chromium.org6b570492010-06-21 18:51:58 +0000623 sub_deps = Dependency(self, deps[d].module_name, sub_deps_base_url
624 ).ParseDepsFile(False)
maruel@chromium.org75a59272010-06-11 22:34:03 +0000625 url = deps[d].GetUrl(d, sub_deps_base_url, self.root_dir(), sub_deps)
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000626 entries[d] = url
627 if run_scm:
628 self._options.revision = revision_overrides.get(d)
maruel@chromium.org75a59272010-06-11 22:34:03 +0000629 scm = gclient_scm.CreateSCM(url, self.root_dir(), d)
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000630 scm.RunCommand(command, self._options, args, file_list)
631 self._options.revision = None
gspencer@google.comdf2d5902009-09-11 22:16:21 +0000632
phajdan.jr@chromium.orgd83b2b22009-08-11 15:30:55 +0000633 # Convert all absolute paths to relative.
634 for i in range(len(file_list)):
635 # TODO(phajdan.jr): We should know exactly when the paths are absolute.
636 # It depends on the command being executed (like runhooks vs sync).
637 if not os.path.isabs(file_list[i]):
638 continue
639
maruel@chromium.org75a59272010-06-11 22:34:03 +0000640 prefix = os.path.commonprefix([self.root_dir().lower(),
phajdan.jr@chromium.orgd83b2b22009-08-11 15:30:55 +0000641 file_list[i].lower()])
642 file_list[i] = file_list[i][len(prefix):]
643
644 # Strip any leading path separators.
645 while file_list[i].startswith('\\') or file_list[i].startswith('/'):
646 file_list[i] = file_list[i][1:]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000647
maruel@chromium.org75a59272010-06-11 22:34:03 +0000648 is_using_git = gclient_utils.IsUsingGit(self.root_dir(), entries.keys())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000649 self._RunHooks(command, file_list, is_using_git)
650
651 if command == 'update':
ajwong@chromium.orgcdcee802009-06-23 15:30:42 +0000652 # Notify the user if there is an orphaned entry in their working copy.
653 # Only delete the directory if there are no changes in it, and
654 # delete_unversioned_trees is set to true.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000655 prev_entries = self._ReadEntries()
656 for entry in prev_entries:
maruel@chromium.orgc5e9aec2009-08-03 18:25:56 +0000657 # Fix path separator on Windows.
658 entry_fixed = entry.replace('/', os.path.sep)
maruel@chromium.org75a59272010-06-11 22:34:03 +0000659 e_dir = os.path.join(self.root_dir(), entry_fixed)
maruel@chromium.orgc5e9aec2009-08-03 18:25:56 +0000660 # Use entry and not entry_fixed there.
maruel@chromium.org0329e672009-05-13 18:41:04 +0000661 if entry not in entries and os.path.exists(e_dir):
msb@chromium.org83017012009-09-28 18:52:12 +0000662 modified_files = False
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000663 if isinstance(prev_entries, list):
msb@chromium.org83017012009-09-28 18:52:12 +0000664 # old .gclient_entries format was list, now dict
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000665 modified_files = gclient_scm.scm.SVN.CaptureStatus(e_dir)
msb@chromium.org83017012009-09-28 18:52:12 +0000666 else:
667 file_list = []
maruel@chromium.org75a59272010-06-11 22:34:03 +0000668 scm = gclient_scm.CreateSCM(prev_entries[entry], self.root_dir(),
msb@chromium.org83017012009-09-28 18:52:12 +0000669 entry_fixed)
670 scm.status(self._options, [], file_list)
671 modified_files = file_list != []
672 if not self._options.delete_unversioned_trees or modified_files:
maruel@chromium.orgc5e9aec2009-08-03 18:25:56 +0000673 # There are modified files in this entry. Keep warning until
674 # removed.
maruel@chromium.orgc5e9aec2009-08-03 18:25:56 +0000675 print(("\nWARNING: \"%s\" is no longer part of this client. "
676 "It is recommended that you manually remove it.\n") %
677 entry_fixed)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000678 else:
679 # Delete the entry
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000680 print("\n________ deleting \'%s\' " +
maruel@chromium.org75a59272010-06-11 22:34:03 +0000681 "in \'%s\'") % (entry_fixed, self.root_dir())
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000682 gclient_utils.RemoveDirectory(e_dir)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000683 # record the current list of entries for next time
684 self._SaveEntries(entries)
maruel@chromium.org17cdf762010-05-28 17:30:52 +0000685 return 0
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000686
687 def PrintRevInfo(self):
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000688 """Output revision info mapping for the client and its dependencies.
689
690 This allows the capture of an overall "revision" for the source tree that
maruel@chromium.org918a9ae2010-05-28 15:50:30 +0000691 can be used to reproduce the same tree in the future. It is only useful for
692 "unpinned dependencies", i.e. DEPS/deps references without a svn revision
693 number or a git hash. A git branch name isn't "pinned" since the actual
694 commit can change.
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000695
696 The --snapshot option allows creating a .gclient file to reproduce the tree.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000697 """
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000698 if not self.dependencies:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000699 raise gclient_utils.Error("No solution specified")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000700
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000701 # Inner helper to generate base url and rev tuple
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000702 def GetURLAndRev(name, original_url):
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000703 url, _ = gclient_utils.SplitUrlRevision(original_url)
maruel@chromium.org75a59272010-06-11 22:34:03 +0000704 scm = gclient_scm.CreateSCM(original_url, self.root_dir(), name)
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000705 return (url, scm.revinfo(self._options, [], None))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000706
maruel@chromium.orge3da35f2010-03-09 21:40:45 +0000707 # text of the snapshot gclient file
708 new_gclient = ""
709 # Dictionary of { path : SCM url } to ensure no duplicate solutions
710 solution_names = {}
nasser@codeaurora.orgde8f3522010-03-11 23:47:44 +0000711 entries = {}
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000712 # Run on the base solutions first.
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000713 for solution in self.dependencies:
maruel@chromium.orge3da35f2010-03-09 21:40:45 +0000714 # Dictionary of { path : SCM url } to describe the gclient checkout
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000715 name = solution.name
maruel@chromium.orge3da35f2010-03-09 21:40:45 +0000716 if name in solution_names:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000717 raise gclient_utils.Error("solution %s specified more than once" % name)
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000718 (url, rev) = GetURLAndRev(name, solution.url)
msb@chromium.org770ff9e2009-09-23 17:18:18 +0000719 entries[name] = "%s@%s" % (url, rev)
maruel@chromium.orge3da35f2010-03-09 21:40:45 +0000720 solution_names[name] = "%s@%s" % (url, rev)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000721
nasser@codeaurora.orgde8f3522010-03-11 23:47:44 +0000722 # Process the dependencies next (sort alphanumerically to ensure that
723 # containing directories get populated first and for readability)
maruel@chromium.org6b570492010-06-21 18:51:58 +0000724 deps = self._ParseAllDeps(entries)
nasser@codeaurora.orgde8f3522010-03-11 23:47:44 +0000725 deps_to_process = deps.keys()
726 deps_to_process.sort()
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000727
nasser@codeaurora.orgde8f3522010-03-11 23:47:44 +0000728 # First pass for direct dependencies.
729 for d in deps_to_process:
730 if type(deps[d]) == str:
731 (url, rev) = GetURLAndRev(d, deps[d])
732 entries[d] = "%s@%s" % (url, rev)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000733
nasser@codeaurora.orgde8f3522010-03-11 23:47:44 +0000734 # Second pass for inherited deps (via the From keyword)
735 for d in deps_to_process:
tony@chromium.org4b5b1772010-04-08 01:52:56 +0000736 if isinstance(deps[d], self.FromImpl):
nasser@codeaurora.orgde8f3522010-03-11 23:47:44 +0000737 deps_parent_url = entries[deps[d].module_name]
738 if deps_parent_url.find("@") < 0:
739 raise gclient_utils.Error("From %s missing revisioned url" %
740 deps[d].module_name)
maruel@chromium.org6b570492010-06-21 18:51:58 +0000741 sub_deps_base_url = deps[deps[d].module_name]
742 sub_deps = Dependency(self, deps[d].module_name, sub_deps_base_url
743 ).ParseDepsFile(False)
744 url = deps[d].GetUrl(d, sub_deps_base_url, self.root_dir(), sub_deps)
745 (url, rev) = GetURLAndRev(d, url)
nasser@codeaurora.orgde8f3522010-03-11 23:47:44 +0000746 entries[d] = "%s@%s" % (url, rev)
maruel@chromium.orge3da35f2010-03-09 21:40:45 +0000747
nasser@codeaurora.orgde8f3522010-03-11 23:47:44 +0000748 # Build the snapshot configuration string
749 if self._options.snapshot:
750 url = entries.pop(name)
751 custom_deps = ",\n ".join(["\"%s\": \"%s\"" % (x, entries[x])
752 for x in sorted(entries.keys())])
maruel@chromium.orge3da35f2010-03-09 21:40:45 +0000753
maruel@chromium.org1f7d1182010-05-17 18:17:38 +0000754 new_gclient += self.DEFAULT_SNAPSHOT_SOLUTION_TEXT % {
nasser@codeaurora.orgde8f3522010-03-11 23:47:44 +0000755 'solution_name': name,
756 'solution_url': url,
757 'safesync_url' : "",
758 'solution_deps': custom_deps,
759 }
760 else:
761 print(";\n".join(["%s: %s" % (x, entries[x])
762 for x in sorted(entries.keys())]))
maruel@chromium.orge3da35f2010-03-09 21:40:45 +0000763
764 # Print the snapshot configuration file
765 if self._options.snapshot:
maruel@chromium.org491c04b2010-05-17 18:17:44 +0000766 config = self.DEFAULT_SNAPSHOT_FILE_TEXT % {'solution_list': new_gclient}
maruel@chromium.org75a59272010-06-11 22:34:03 +0000767 snapclient = GClient(self.root_dir(), self._options)
maruel@chromium.orge3da35f2010-03-09 21:40:45 +0000768 snapclient.SetConfig(config)
maruel@chromium.org116704f2010-06-11 17:34:38 +0000769 print(snapclient.config_content)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000770
maruel@chromium.org75a59272010-06-11 22:34:03 +0000771 def root_dir(self):
772 return self._root_dir
773
maruel@chromium.org6b570492010-06-21 18:51:58 +0000774 def enforced_os(self):
775 return self._enforced_os
776
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000777
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000778#### gclient commands.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000779
780
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000781def CMDcleanup(parser, args):
782 """Cleans up all working copies.
maruel@chromium.orgddff62d2010-05-17 21:02:36 +0000783
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000784Mostly svn-specific. Simply runs 'svn cleanup' for each module.
maruel@chromium.org79692d62010-05-14 18:57:13 +0000785"""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000786 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
787 help='override deps for the specified (comma-separated) '
788 'platform(s); \'all\' will process all deps_os '
789 'references')
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000790 (options, args) = parser.parse_args(args)
maruel@chromium.org2806acc2009-05-15 12:33:34 +0000791 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000792 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000793 raise gclient_utils.Error('client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000794 if options.verbose:
795 # Print out the .gclient file. This is longer than if we just printed the
796 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.org116704f2010-06-11 17:34:38 +0000797 print(client.config_content)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000798 return client.RunOnDeps('cleanup', args)
799
800
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000801@attr('usage', '[url] [safesync url]')
802def CMDconfig(parser, args):
maruel@chromium.orgddff62d2010-05-17 21:02:36 +0000803 """Create a .gclient file in the current directory.
804
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000805This specifies the configuration for further commands. After update/sync,
maruel@chromium.org79692d62010-05-14 18:57:13 +0000806top-level DEPS files in each module are read to determine dependent
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000807modules to operate on as well. If optional [url] parameter is
maruel@chromium.org79692d62010-05-14 18:57:13 +0000808provided, then configuration is read from a specified Subversion server
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000809URL.
maruel@chromium.org79692d62010-05-14 18:57:13 +0000810"""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000811 parser.add_option('--spec',
812 help='create a gclient file containing the provided '
813 'string. Due to Cygwin/Python brokenness, it '
814 'probably can\'t contain any newlines.')
815 parser.add_option('--name',
816 help='overrides the default name for the solution')
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000817 (options, args) = parser.parse_args(args)
maruel@chromium.org5fc2a332010-05-26 19:37:15 +0000818 if ((options.spec and args) or len(args) > 2 or
819 (not options.spec and not args)):
820 parser.error('Inconsistent arguments. Use either --spec or one or 2 args')
821
maruel@chromium.org0329e672009-05-13 18:41:04 +0000822 if os.path.exists(options.config_filename):
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000823 raise gclient_utils.Error('%s file already exists in the current directory'
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000824 % options.config_filename)
maruel@chromium.org2806acc2009-05-15 12:33:34 +0000825 client = GClient('.', options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000826 if options.spec:
827 client.SetConfig(options.spec)
828 else:
maruel@chromium.org1ab7ffc2009-06-03 17:21:37 +0000829 base_url = args[0].rstrip('/')
iposva@chromium.org8cf7a392010-04-07 17:20:26 +0000830 if not options.name:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000831 name = base_url.split('/')[-1]
iposva@chromium.org8cf7a392010-04-07 17:20:26 +0000832 else:
833 # specify an alternate relpath for the given URL.
834 name = options.name
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000835 safesync_url = ''
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000836 if len(args) > 1:
837 safesync_url = args[1]
838 client.SetDefaultConfig(name, base_url, safesync_url)
839 client.SaveConfig()
maruel@chromium.org79692d62010-05-14 18:57:13 +0000840 return 0
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000841
842
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000843def CMDexport(parser, args):
maruel@chromium.orgddff62d2010-05-17 21:02:36 +0000844 """Wrapper for svn export for all managed directories."""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000845 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
846 help='override deps for the specified (comma-separated) '
847 'platform(s); \'all\' will process all deps_os '
848 'references')
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000849 (options, args) = parser.parse_args(args)
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +0000850 if len(args) != 1:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000851 raise gclient_utils.Error('Need directory name')
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +0000852 client = GClient.LoadCurrentConfig(options)
853
854 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000855 raise gclient_utils.Error('client not configured; see \'gclient config\'')
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +0000856
857 if options.verbose:
858 # Print out the .gclient file. This is longer than if we just printed the
859 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.org116704f2010-06-11 17:34:38 +0000860 print(client.config_content)
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +0000861 return client.RunOnDeps('export', args)
862
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000863
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000864@attr('epilog', """Example:
865 gclient pack > patch.txt
866 generate simple patch for configured client and dependences
867""")
868def CMDpack(parser, args):
maruel@chromium.org79692d62010-05-14 18:57:13 +0000869 """Generate a patch which can be applied at the root of the tree.
maruel@chromium.orgddff62d2010-05-17 21:02:36 +0000870
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000871Internally, runs 'svn diff'/'git diff' on each checked out module and
maruel@chromium.org79692d62010-05-14 18:57:13 +0000872dependencies, and performs minimal postprocessing of the output. The
873resulting patch is printed to stdout and can be applied to a freshly
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000874checked out tree via 'patch -p0 < patchfile'.
maruel@chromium.org79692d62010-05-14 18:57:13 +0000875"""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000876 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
877 help='override deps for the specified (comma-separated) '
878 'platform(s); \'all\' will process all deps_os '
879 'references')
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000880 (options, args) = parser.parse_args(args)
kbr@google.comab318592009-09-04 00:54:55 +0000881 client = GClient.LoadCurrentConfig(options)
882 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000883 raise gclient_utils.Error('client not configured; see \'gclient config\'')
kbr@google.comab318592009-09-04 00:54:55 +0000884 if options.verbose:
885 # Print out the .gclient file. This is longer than if we just printed the
886 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.org116704f2010-06-11 17:34:38 +0000887 print(client.config_content)
kbr@google.comab318592009-09-04 00:54:55 +0000888 return client.RunOnDeps('pack', args)
889
890
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000891def CMDstatus(parser, args):
892 """Show modification status for every dependencies."""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000893 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
894 help='override deps for the specified (comma-separated) '
895 'platform(s); \'all\' will process all deps_os '
896 'references')
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000897 (options, args) = parser.parse_args(args)
maruel@chromium.org2806acc2009-05-15 12:33:34 +0000898 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000899 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000900 raise gclient_utils.Error('client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000901 if options.verbose:
902 # Print out the .gclient file. This is longer than if we just printed the
903 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.org116704f2010-06-11 17:34:38 +0000904 print(client.config_content)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000905 return client.RunOnDeps('status', args)
906
907
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000908@attr('epilog', """Examples:
maruel@chromium.org79692d62010-05-14 18:57:13 +0000909 gclient sync
910 update files from SCM according to current configuration,
911 *for modules which have changed since last update or sync*
912 gclient sync --force
913 update files from SCM according to current configuration, for
914 all modules (useful for recovering files deleted from local copy)
915 gclient sync --revision src@31000
916 update src directory to r31000
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000917""")
918def CMDsync(parser, args):
919 """Checkout/update all modules."""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000920 parser.add_option('-f', '--force', action='store_true',
921 help='force update even for unchanged modules')
922 parser.add_option('-n', '--nohooks', action='store_true',
923 help='don\'t run hooks after the update is complete')
924 parser.add_option('-r', '--revision', action='append',
925 dest='revisions', metavar='REV', default=[],
926 help='Enforces revision/hash for the solutions with the '
927 'format src@rev. The src@ part is optional and can be '
928 'skipped. -r can be used multiple times when .gclient '
929 'has multiple solutions configured and will work even '
930 'if the src@ part is skipped.')
931 parser.add_option('-H', '--head', action='store_true',
932 help='skips any safesync_urls specified in '
933 'configured solutions and sync to head instead')
934 parser.add_option('-D', '--delete_unversioned_trees', action='store_true',
935 help='delete any unexpected unversioned trees '
936 'that are in the checkout')
937 parser.add_option('-R', '--reset', action='store_true',
938 help='resets any local changes before updating (git only)')
939 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
940 help='override deps for the specified (comma-separated) '
941 'platform(s); \'all\' will process all deps_os '
942 'references')
943 parser.add_option('-m', '--manually_grab_svn_rev', action='store_true',
944 help='Skip svn up whenever possible by requesting '
945 'actual HEAD revision from the repository')
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000946 (options, args) = parser.parse_args(args)
maruel@chromium.org2806acc2009-05-15 12:33:34 +0000947 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000948
949 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000950 raise gclient_utils.Error('client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000951
maruel@chromium.org307d1792010-05-31 20:03:13 +0000952 if options.revisions and options.head:
953 # TODO(maruel): Make it a parser.error if it doesn't break any builder.
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000954 print('Warning: you cannot use both --head and --revision')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000955
956 if options.verbose:
957 # Print out the .gclient file. This is longer than if we just printed the
958 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.org116704f2010-06-11 17:34:38 +0000959 print(client.config_content)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000960 return client.RunOnDeps('update', args)
961
962
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000963def CMDupdate(parser, args):
maruel@chromium.orgddff62d2010-05-17 21:02:36 +0000964 """Alias for the sync command. Deprecated."""
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000965 return CMDsync(parser, args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000966
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000967def CMDdiff(parser, args):
968 """Displays local diff for every dependencies."""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000969 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
970 help='override deps for the specified (comma-separated) '
971 'platform(s); \'all\' will process all deps_os '
972 'references')
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000973 (options, args) = parser.parse_args(args)
maruel@chromium.org2806acc2009-05-15 12:33:34 +0000974 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000975 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000976 raise gclient_utils.Error('client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000977 if options.verbose:
978 # Print out the .gclient file. This is longer than if we just printed the
979 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.org116704f2010-06-11 17:34:38 +0000980 print(client.config_content)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000981 return client.RunOnDeps('diff', args)
982
983
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000984def CMDrevert(parser, args):
985 """Revert all modifications in every dependencies."""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000986 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
987 help='override deps for the specified (comma-separated) '
988 'platform(s); \'all\' will process all deps_os '
989 'references')
990 parser.add_option('-n', '--nohooks', action='store_true',
991 help='don\'t run hooks after the revert is complete')
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000992 (options, args) = parser.parse_args(args)
993 # --force is implied.
994 options.force = True
maruel@chromium.org2806acc2009-05-15 12:33:34 +0000995 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000996 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000997 raise gclient_utils.Error('client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000998 return client.RunOnDeps('revert', args)
999
1000
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001001def CMDrunhooks(parser, args):
1002 """Runs hooks for files that have been modified in the local working copy."""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00001003 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
1004 help='override deps for the specified (comma-separated) '
1005 'platform(s); \'all\' will process all deps_os '
1006 'references')
1007 parser.add_option('-f', '--force', action='store_true', default=True,
1008 help='Deprecated. No effect.')
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001009 (options, args) = parser.parse_args(args)
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001010 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001011 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00001012 raise gclient_utils.Error('client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001013 if options.verbose:
1014 # Print out the .gclient file. This is longer than if we just printed the
1015 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.org116704f2010-06-11 17:34:38 +00001016 print(client.config_content)
maruel@chromium.org5df6a462009-08-28 18:52:26 +00001017 options.force = True
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001018 options.nohooks = False
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001019 return client.RunOnDeps('runhooks', args)
1020
1021
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001022def CMDrevinfo(parser, args):
maruel@chromium.org9eda4112010-06-11 18:56:10 +00001023 """Output revision info mapping for the client and its dependencies.
1024
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00001025 This allows the capture of an overall 'revision' for the source tree that
maruel@chromium.org9eda4112010-06-11 18:56:10 +00001026 can be used to reproduce the same tree in the future. It is only useful for
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00001027 'unpinned dependencies', i.e. DEPS/deps references without a svn revision
1028 number or a git hash. A git branch name isn't 'pinned' since the actual
maruel@chromium.org9eda4112010-06-11 18:56:10 +00001029 commit can change.
1030 """
1031 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
1032 help='override deps for the specified (comma-separated) '
1033 'platform(s); \'all\' will process all deps_os '
1034 'references')
1035 parser.add_option('-s', '--snapshot', action='store_true',
1036 help='creates a snapshot .gclient file of the current '
1037 'version of all repositories to reproduce the tree, '
1038 'implies -a')
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001039 (options, args) = parser.parse_args(args)
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001040 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001041 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00001042 raise gclient_utils.Error('client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001043 client.PrintRevInfo()
maruel@chromium.org79692d62010-05-14 18:57:13 +00001044 return 0
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001045
1046
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001047def Command(name):
1048 return getattr(sys.modules[__name__], 'CMD' + name, None)
1049
1050
1051def CMDhelp(parser, args):
1052 """Prints list of commands or help for a specific command."""
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001053 (_, args) = parser.parse_args(args)
maruel@chromium.orgddff62d2010-05-17 21:02:36 +00001054 if len(args) == 1:
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001055 return Main(args + ['--help'])
maruel@chromium.orgddff62d2010-05-17 21:02:36 +00001056 parser.print_help()
1057 return 0
1058
1059
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001060def GenUsage(parser, command):
1061 """Modify an OptParse object with the function's documentation."""
1062 obj = Command(command)
1063 if command == 'help':
1064 command = '<command>'
1065 # OptParser.description prefer nicely non-formatted strings.
1066 parser.description = re.sub('[\r\n ]{2,}', ' ', obj.__doc__)
1067 usage = getattr(obj, 'usage', '')
1068 parser.set_usage('%%prog %s [options] %s' % (command, usage))
1069 parser.epilog = getattr(obj, 'epilog', None)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001070
1071
1072def Main(argv):
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001073 """Doesn't parse the arguments here, just find the right subcommand to
1074 execute."""
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001075 try:
1076 # Do it late so all commands are listed.
1077 CMDhelp.usage = ('\n\nCommands are:\n' + '\n'.join([
1078 ' %-10s %s' % (fn[3:], Command(fn[3:]).__doc__.split('\n')[0].strip())
1079 for fn in dir(sys.modules[__name__]) if fn.startswith('CMD')]))
1080 parser = optparse.OptionParser(version='%prog ' + __version__)
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001081 parser.add_option('-v', '--verbose', action='count', default=0,
1082 help='Produces additional output for diagnostics. Can be '
1083 'used up to three times for more logging info.')
1084 parser.add_option('--gclientfile', dest='config_filename',
1085 default=os.environ.get('GCLIENT_FILE', '.gclient'),
1086 help='Specify an alternate %default file')
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001087 # Integrate standard options processing.
1088 old_parser = parser.parse_args
1089 def Parse(args):
1090 (options, args) = old_parser(args)
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001091 level = None
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001092 if options.verbose == 2:
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001093 level = logging.INFO
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001094 elif options.verbose > 2:
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001095 level = logging.DEBUG
1096 logging.basicConfig(level=level,
1097 format='%(module)s(%(lineno)d) %(funcName)s:%(message)s')
1098 options.entries_filename = options.config_filename + '_entries'
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001099 if not hasattr(options, 'revisions'):
1100 # GClient.RunOnDeps expects it even if not applicable.
1101 options.revisions = []
1102 if not hasattr(options, 'head'):
1103 options.head = None
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001104 if not hasattr(options, 'nohooks'):
1105 options.nohooks = True
1106 if not hasattr(options, 'deps_os'):
1107 options.deps_os = None
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001108 return (options, args)
1109 parser.parse_args = Parse
1110 # We don't want wordwrapping in epilog (usually examples)
1111 parser.format_epilog = lambda _: parser.epilog or ''
1112 if argv:
1113 command = Command(argv[0])
1114 if command:
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001115 # 'fix' the usage and the description now that we know the subcommand.
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001116 GenUsage(parser, argv[0])
1117 return command(parser, argv[1:])
1118 # Not a known command. Default to help.
1119 GenUsage(parser, 'help')
1120 return CMDhelp(parser, argv)
1121 except gclient_utils.Error, e:
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001122 print >> sys.stderr, 'Error: %s' % str(e)
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001123 return 1
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001124
1125
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001126if '__main__' == __name__:
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001127 sys.exit(Main(sys.argv[1:]))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001128
1129# vim: ts=2:sw=2:tw=80:et: