blob: 55305bd8c4b2493766dfa995b9ad735a50613da0 [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.org271375b2010-06-23 19:17:38 +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.org271375b2010-06-23 19:17:38 +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 self.deps_parsed = True
191 filepath = os.path.join(self.root_dir(), self.name, self.deps_file)
192 if not os.path.isfile(filepath):
maruel@chromium.org0d425922010-06-21 19:22:24 +0000193 return {}
maruel@chromium.org271375b2010-06-23 19:17:38 +0000194 deps_content = gclient_utils.FileRead(filepath)
maruel@chromium.org0d425922010-06-21 19:22:24 +0000195
maruel@chromium.org271375b2010-06-23 19:17:38 +0000196 # Eval the content.
197 # One thing is unintuitive, vars= {} must happen before Var() use.
198 local_scope = {}
199 var = self.VarImpl(self.custom_vars, local_scope)
200 global_scope = {
201 'File': self.FileImpl,
202 'From': self.FromImpl,
203 'Var': var.Lookup,
204 'deps_os': {},
205 }
206 exec(deps_content, global_scope, local_scope)
207 deps = local_scope.get('deps', {})
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000208 # load os specific dependencies if defined. these dependencies may
209 # override or extend the values defined by the 'deps' member.
maruel@chromium.org271375b2010-06-23 19:17:38 +0000210 if 'deps_os' in local_scope:
211 for deps_os_key in self.enforced_os():
212 os_deps = local_scope['deps_os'].get(deps_os_key, {})
213 if len(self.enforced_os()) > 1:
214 # Ignore any conflict when including deps for more than one
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000215 # platform, so we collect the broadest set of dependencies available.
216 # We may end up with the wrong revision of something for our
217 # platform, but this is the best we can do.
218 deps.update([x for x in os_deps.items() if not x[0] in deps])
219 else:
220 deps.update(os_deps)
221
maruel@chromium.org271375b2010-06-23 19:17:38 +0000222 self.deps_hooks.extend(local_scope.get('hooks', []))
223
224 # If a line is in custom_deps, but not in the solution, we want to append
225 # this line to the solution.
226 for d in self.custom_deps:
227 if d not in deps:
228 deps[d] = self.custom_deps[d]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000229
230 # If use_relative_paths is set in the DEPS file, regenerate
231 # the dictionary using paths relative to the directory containing
232 # the DEPS file.
maruel@chromium.org271375b2010-06-23 19:17:38 +0000233 use_relative_paths = local_scope.get('use_relative_paths', False)
234 if use_relative_paths:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000235 rel_deps = {}
236 for d, url in deps.items():
237 # normpath is required to allow DEPS to use .. in their
238 # dependency local path.
maruel@chromium.org271375b2010-06-23 19:17:38 +0000239 rel_deps[os.path.normpath(os.path.join(self.name, d))] = url
240 deps = rel_deps
241 # TODO(maruel): Add these dependencies into self.dependencies.
242 return deps
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000243
maruel@chromium.org271375b2010-06-23 19:17:38 +0000244 def _ParseAllDeps(self, solution_urls):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000245 """Parse the complete list of dependencies for the client.
246
247 Args:
248 solution_urls: A dict mapping module names (as relative paths) to URLs
249 corresponding to the solutions specified by the client. This parameter
250 is passed as an optimization.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000251
252 Returns:
253 A dict mapping module names (as relative paths) to URLs corresponding
254 to the entire set of dependencies to checkout for the given client.
255
256 Raises:
257 Error: If a dependency conflicts with another dependency or of a solution.
258 """
259 deps = {}
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000260 for solution in self.dependencies:
maruel@chromium.org271375b2010-06-23 19:17:38 +0000261 solution_deps = solution.ParseDepsFile(True)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000262
263 # If a line is in custom_deps, but not in the solution, we want to append
264 # this line to the solution.
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000265 for d in solution.custom_deps:
266 if d not in solution_deps:
267 solution_deps[d] = solution.custom_deps[d]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000268
269 for d in solution_deps:
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000270 if d in solution.custom_deps:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000271 # Dependency is overriden.
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000272 url = solution.custom_deps[d]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000273 if url is None:
274 continue
275 else:
276 url = solution_deps[d]
277 # if we have a From reference dependent on another solution, then
278 # just skip the From reference. When we pull deps for the solution,
279 # we will take care of this dependency.
280 #
281 # If multiple solutions all have the same From reference, then we
282 # should only add one to our list of dependencies.
tony@chromium.org4b5b1772010-04-08 01:52:56 +0000283 if isinstance(url, self.FromImpl):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000284 if url.module_name in solution_urls:
285 # Already parsed.
286 continue
287 if d in deps and type(deps[d]) != str:
288 if url.module_name == deps[d].module_name:
289 continue
tony@chromium.org4b5b1772010-04-08 01:52:56 +0000290 elif isinstance(url, str):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000291 parsed_url = urlparse.urlparse(url)
292 scheme = parsed_url[0]
293 if not scheme:
294 # A relative url. Fetch the real base.
295 path = parsed_url[2]
296 if path[0] != "/":
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000297 raise gclient_utils.Error(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000298 "relative DEPS entry \"%s\" must begin with a slash" % d)
msb@chromium.orge6f78352010-01-13 17:05:33 +0000299 # Create a scm just to query the full url.
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000300 scm = gclient_scm.CreateSCM(solution.url, self.root_dir(),
301 None)
msb@chromium.orge6f78352010-01-13 17:05:33 +0000302 url = scm.FullUrlForRelativeUrl(url)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000303 if d in deps and deps[d] != url:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000304 raise gclient_utils.Error(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000305 "Solutions have conflicting versions of dependency \"%s\"" % d)
306 if d in solution_urls and solution_urls[d] != url:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000307 raise gclient_utils.Error(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000308 "Dependency \"%s\" conflicts with specified solution" % d)
309 # Grab the dependency.
310 deps[d] = url
311 return deps
312
phajdan.jr@chromium.org71b40682009-07-31 23:40:09 +0000313 def _RunHookAction(self, hook_dict, matching_file_list):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000314 """Runs the action from a single hook.
315 """
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000316 logging.info(hook_dict)
317 logging.info(matching_file_list)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000318 command = hook_dict['action'][:]
319 if command[0] == 'python':
320 # If the hook specified "python" as the first item, the action is a
321 # Python script. Run it by starting a new copy of the same
322 # interpreter.
323 command[0] = sys.executable
324
phajdan.jr@chromium.org71b40682009-07-31 23:40:09 +0000325 if '$matching_files' in command:
phajdan.jr@chromium.org68f2e092009-08-06 17:05:35 +0000326 splice_index = command.index('$matching_files')
327 command[splice_index:splice_index + 1] = matching_file_list
phajdan.jr@chromium.org71b40682009-07-31 23:40:09 +0000328
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000329 # Use a discrete exit status code of 2 to indicate that a hook action
330 # failed. Users of this script may wish to treat hook action failures
331 # differently from VC failures.
maruel@chromium.org75a59272010-06-11 22:34:03 +0000332 gclient_utils.SubprocessCall(command, self.root_dir(), fail_status=2)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000333
334 def _RunHooks(self, command, file_list, is_using_git):
335 """Evaluates all hooks, running actions as needed.
336 """
337 # Hooks only run for these command types.
338 if not command in ('update', 'revert', 'runhooks'):
339 return
340
evan@chromium.org67820ef2009-07-27 17:23:00 +0000341 # Hooks only run when --nohooks is not specified
342 if self._options.nohooks:
343 return
344
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000345 # Get any hooks from the .gclient file.
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000346 hooks = self.deps_hooks[:]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000347 # Add any hooks found in DEPS files.
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000348 for d in self.dependencies:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000349 hooks.extend(d.deps_hooks)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000350
351 # If "--force" was specified, run all hooks regardless of what files have
352 # changed. If the user is using git, then we don't know what files have
353 # changed so we always run all hooks.
354 if self._options.force or is_using_git:
355 for hook_dict in hooks:
phajdan.jr@chromium.org71b40682009-07-31 23:40:09 +0000356 self._RunHookAction(hook_dict, [])
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000357 return
358
359 # Run hooks on the basis of whether the files from the gclient operation
360 # match each hook's pattern.
361 for hook_dict in hooks:
362 pattern = re.compile(hook_dict['pattern'])
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000363 matching_file_list = [f for f in file_list if pattern.search(f)]
phajdan.jr@chromium.org71b40682009-07-31 23:40:09 +0000364 if matching_file_list:
365 self._RunHookAction(hook_dict, matching_file_list)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000366
maruel@chromium.org271375b2010-06-23 19:17:38 +0000367 def root_dir(self):
368 return self.parent.root_dir()
369
370 def enforced_os(self):
371 return self.parent.enforced_os()
372
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000373
374class GClient(Dependency):
375 """Main gclient checkout root where .gclient resides."""
376 SUPPORTED_COMMANDS = [
377 'cleanup', 'diff', 'export', 'pack', 'revert', 'status', 'update',
378 'runhooks'
379 ]
380
381 DEPS_OS_CHOICES = {
382 "win32": "win",
383 "win": "win",
384 "cygwin": "win",
385 "darwin": "mac",
386 "mac": "mac",
387 "unix": "unix",
388 "linux": "unix",
389 "linux2": "unix",
390 }
391
392 DEFAULT_CLIENT_FILE_TEXT = ("""\
393solutions = [
394 { "name" : "%(solution_name)s",
395 "url" : "%(solution_url)s",
396 "custom_deps" : {
397 },
398 "safesync_url": "%(safesync_url)s"
399 },
400]
401""")
402
403 DEFAULT_SNAPSHOT_SOLUTION_TEXT = ("""\
404 { "name" : "%(solution_name)s",
405 "url" : "%(solution_url)s",
406 "custom_deps" : {
407 %(solution_deps)s,
408 },
409 "safesync_url": "%(safesync_url)s"
410 },
411""")
412
413 DEFAULT_SNAPSHOT_FILE_TEXT = ("""\
414# Snapshot generated with gclient revinfo --snapshot
415solutions = [
416%(solution_list)s
417]
418""")
419
420 def __init__(self, root_dir, options):
421 Dependency.__init__(self, None, None, None)
maruel@chromium.org0d425922010-06-21 19:22:24 +0000422 self._options = options
maruel@chromium.org271375b2010-06-23 19:17:38 +0000423 if options.deps_os:
424 enforced_os = options.deps_os.split(',')
425 else:
426 enforced_os = [self.DEPS_OS_CHOICES.get(sys.platform, 'unix')]
427 if 'all' in enforced_os:
428 enforced_os = self.DEPS_OS_CHOICES.itervalues()
429 self._enforced_os = list(set(enforced_os))
430 self._root_dir = root_dir
maruel@chromium.org9a66ddf2010-06-16 16:54:16 +0000431 self.config_content = None
432
433 def SetConfig(self, content):
434 assert self.dependencies == []
435 config_dict = {}
436 self.config_content = content
437 try:
438 exec(content, config_dict)
439 except SyntaxError, e:
440 try:
441 # Try to construct a human readable error message
442 error_message = [
443 'There is a syntax error in your configuration file.',
444 'Line #%s, character %s:' % (e.lineno, e.offset),
445 '"%s"' % re.sub(r'[\r\n]*$', '', e.text) ]
446 except:
447 # Something went wrong, re-raise the original exception
448 raise e
449 else:
450 # Raise a new exception with the human readable message:
451 raise gclient_utils.Error('\n'.join(error_message))
452 for s in config_dict.get('solutions', []):
453 self.dependencies.append(Dependency(
454 self, s['name'], s['url'],
455 s.get('safesync_url', None),
456 s.get('custom_deps', {}),
457 s.get('custom_vars', {})))
458 # .gclient can have hooks.
459 self.deps_hooks = config_dict.get('hooks', [])
460
461 def SaveConfig(self):
462 gclient_utils.FileWrite(os.path.join(self.root_dir(),
463 self._options.config_filename),
464 self.config_content)
465
466 @staticmethod
467 def LoadCurrentConfig(options):
468 """Searches for and loads a .gclient file relative to the current working
469 dir. Returns a GClient object."""
470 path = gclient_utils.FindGclientRoot(os.getcwd(), options.config_filename)
471 if not path:
472 return None
473 client = GClient(path, options)
474 client.SetConfig(gclient_utils.FileRead(
475 os.path.join(path, options.config_filename)))
476 return client
477
478 def SetDefaultConfig(self, solution_name, solution_url, safesync_url):
479 self.SetConfig(self.DEFAULT_CLIENT_FILE_TEXT % {
480 'solution_name': solution_name,
481 'solution_url': solution_url,
482 'safesync_url' : safesync_url,
483 })
484
485 def _SaveEntries(self, entries):
486 """Creates a .gclient_entries file to record the list of unique checkouts.
487
488 The .gclient_entries file lives in the same directory as .gclient.
489
490 Args:
491 entries: A sequence of solution names.
492 """
493 # Sometimes pprint.pformat will use {', sometimes it'll use { ' ... It
494 # makes testing a bit too fun.
495 result = pprint.pformat(entries, 2)
496 if result.startswith('{\''):
497 result = '{ \'' + result[2:]
498 text = "entries = \\\n" + result + '\n'
499 file_path = os.path.join(self.root_dir(), self._options.entries_filename)
500 gclient_utils.FileWrite(file_path, text)
501
502 def _ReadEntries(self):
503 """Read the .gclient_entries file for the given client.
504
505 Returns:
506 A sequence of solution names, which will be empty if there is the
507 entries file hasn't been created yet.
508 """
509 scope = {}
510 filename = os.path.join(self.root_dir(), self._options.entries_filename)
511 if not os.path.exists(filename):
512 return []
513 exec(gclient_utils.FileRead(filename), scope)
514 return scope['entries']
515
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000516 def _EnforceRevisions(self):
maruel@chromium.org918a9ae2010-05-28 15:50:30 +0000517 """Checks for revision overrides."""
518 revision_overrides = {}
maruel@chromium.org307d1792010-05-31 20:03:13 +0000519 if self._options.head:
520 return revision_overrides
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000521 for s in self.dependencies:
522 if not s.safesync_url:
maruel@chromium.org307d1792010-05-31 20:03:13 +0000523 continue
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000524 handle = urllib.urlopen(s.safesync_url)
maruel@chromium.org307d1792010-05-31 20:03:13 +0000525 rev = handle.read().strip()
526 handle.close()
527 if len(rev):
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000528 self._options.revisions.append('%s@%s' % (s.name, rev))
maruel@chromium.org307d1792010-05-31 20:03:13 +0000529 if not self._options.revisions:
530 return revision_overrides
531 # --revision will take over safesync_url.
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000532 solutions_names = [s.name for s in self.dependencies]
maruel@chromium.org307d1792010-05-31 20:03:13 +0000533 index = 0
534 for revision in self._options.revisions:
535 if not '@' in revision:
536 # Support for --revision 123
537 revision = '%s@%s' % (solutions_names[index], revision)
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000538 sol, rev = revision.split('@', 1)
maruel@chromium.org307d1792010-05-31 20:03:13 +0000539 if not sol in solutions_names:
540 #raise gclient_utils.Error('%s is not a valid solution.' % sol)
541 print >> sys.stderr, ('Please fix your script, having invalid '
542 '--revision flags will soon considered an error.')
543 else:
maruel@chromium.org918a9ae2010-05-28 15:50:30 +0000544 revision_overrides[sol] = rev
maruel@chromium.org307d1792010-05-31 20:03:13 +0000545 index += 1
maruel@chromium.org918a9ae2010-05-28 15:50:30 +0000546 return revision_overrides
547
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000548 def RunOnDeps(self, command, args):
549 """Runs a command on each dependency in a client and its dependencies.
550
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000551 Args:
552 command: The command to use (e.g., 'status' or 'diff')
553 args: list of str - extra arguments to add to the command line.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000554 """
maruel@chromium.org116704f2010-06-11 17:34:38 +0000555 if not command in self.SUPPORTED_COMMANDS:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000556 raise gclient_utils.Error("'%s' is an unsupported command" % command)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000557
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000558 if not self.dependencies:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000559 raise gclient_utils.Error("No solution specified")
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000560 revision_overrides = self._EnforceRevisions()
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000561
562 # When running runhooks --force, there's no need to consult the SCM.
563 # All known hooks are expected to run unconditionally regardless of working
564 # copy state, so skip the SCM status check.
565 run_scm = not (command == 'runhooks' and self._options.force)
566
567 entries = {}
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000568 file_list = []
569 # Run on the base solutions first.
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000570 for solution in self.dependencies:
571 name = solution.name
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000572 if name in entries:
573 raise gclient_utils.Error("solution %s specified more than once" % name)
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000574 url = solution.url
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000575 entries[name] = url
576 if run_scm and url:
577 self._options.revision = revision_overrides.get(name)
maruel@chromium.org75a59272010-06-11 22:34:03 +0000578 scm = gclient_scm.CreateSCM(url, self.root_dir(), name)
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000579 scm.RunCommand(command, self._options, args, file_list)
580 file_list = [os.path.join(name, f.strip()) for f in file_list]
581 self._options.revision = None
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000582
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000583 # Process the dependencies next (sort alphanumerically to ensure that
584 # containing directories get populated first and for readability)
maruel@chromium.org271375b2010-06-23 19:17:38 +0000585 deps = self._ParseAllDeps(entries)
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000586 deps_to_process = deps.keys()
587 deps_to_process.sort()
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000588
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000589 # First pass for direct dependencies.
590 if command == 'update' and not self._options.verbose:
591 pm = Progress('Syncing projects', len(deps_to_process))
592 for d in deps_to_process:
nasser@codeaurora.org1f7a3d12010-02-04 15:11:50 +0000593 if command == 'update' and not self._options.verbose:
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000594 pm.update()
595 if type(deps[d]) == str:
596 url = deps[d]
597 entries[d] = url
598 if run_scm:
599 self._options.revision = revision_overrides.get(d)
maruel@chromium.org75a59272010-06-11 22:34:03 +0000600 scm = gclient_scm.CreateSCM(url, self.root_dir(), d)
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000601 scm.RunCommand(command, self._options, args, file_list)
602 self._options.revision = None
603 elif isinstance(deps[d], self.FileImpl):
maruel@chromium.org491c04b2010-05-17 18:17:44 +0000604 file_dep = deps[d]
605 self._options.revision = file_dep.GetRevision()
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000606 if run_scm:
maruel@chromium.org75a59272010-06-11 22:34:03 +0000607 scm = gclient_scm.CreateSCM(file_dep.GetPath(), self.root_dir(), d)
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000608 scm.RunCommand("updatesingle", self._options,
maruel@chromium.org491c04b2010-05-17 18:17:44 +0000609 args + [file_dep.GetFilename()], file_list)
maruel@chromium.org79692d62010-05-14 18:57:13 +0000610
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000611 if command == 'update' and not self._options.verbose:
612 pm.end()
piman@chromium.org6f363722010-04-27 00:41:09 +0000613
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000614 # Second pass for inherited deps (via the From keyword)
615 for d in deps_to_process:
616 if isinstance(deps[d], self.FromImpl):
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000617 # Getting the URL from the sub_deps file can involve having to resolve
618 # a File() or having to resolve a relative URL. To resolve relative
619 # URLs, we need to pass in the orignal sub deps URL.
620 sub_deps_base_url = deps[deps[d].module_name]
maruel@chromium.org271375b2010-06-23 19:17:38 +0000621 sub_deps = Dependency(self, deps[d].module_name, sub_deps_base_url
622 ).ParseDepsFile(False)
maruel@chromium.org75a59272010-06-11 22:34:03 +0000623 url = deps[d].GetUrl(d, sub_deps_base_url, self.root_dir(), sub_deps)
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000624 entries[d] = url
625 if run_scm:
626 self._options.revision = revision_overrides.get(d)
maruel@chromium.org75a59272010-06-11 22:34:03 +0000627 scm = gclient_scm.CreateSCM(url, self.root_dir(), d)
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000628 scm.RunCommand(command, self._options, args, file_list)
629 self._options.revision = None
gspencer@google.comdf2d5902009-09-11 22:16:21 +0000630
phajdan.jr@chromium.orgd83b2b22009-08-11 15:30:55 +0000631 # Convert all absolute paths to relative.
632 for i in range(len(file_list)):
633 # TODO(phajdan.jr): We should know exactly when the paths are absolute.
634 # It depends on the command being executed (like runhooks vs sync).
635 if not os.path.isabs(file_list[i]):
636 continue
637
maruel@chromium.org75a59272010-06-11 22:34:03 +0000638 prefix = os.path.commonprefix([self.root_dir().lower(),
phajdan.jr@chromium.orgd83b2b22009-08-11 15:30:55 +0000639 file_list[i].lower()])
640 file_list[i] = file_list[i][len(prefix):]
641
642 # Strip any leading path separators.
643 while file_list[i].startswith('\\') or file_list[i].startswith('/'):
644 file_list[i] = file_list[i][1:]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000645
maruel@chromium.org75a59272010-06-11 22:34:03 +0000646 is_using_git = gclient_utils.IsUsingGit(self.root_dir(), entries.keys())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000647 self._RunHooks(command, file_list, is_using_git)
648
649 if command == 'update':
ajwong@chromium.orgcdcee802009-06-23 15:30:42 +0000650 # Notify the user if there is an orphaned entry in their working copy.
651 # Only delete the directory if there are no changes in it, and
652 # delete_unversioned_trees is set to true.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000653 prev_entries = self._ReadEntries()
654 for entry in prev_entries:
maruel@chromium.orgc5e9aec2009-08-03 18:25:56 +0000655 # Fix path separator on Windows.
656 entry_fixed = entry.replace('/', os.path.sep)
maruel@chromium.org75a59272010-06-11 22:34:03 +0000657 e_dir = os.path.join(self.root_dir(), entry_fixed)
maruel@chromium.orgc5e9aec2009-08-03 18:25:56 +0000658 # Use entry and not entry_fixed there.
maruel@chromium.org0329e672009-05-13 18:41:04 +0000659 if entry not in entries and os.path.exists(e_dir):
msb@chromium.org83017012009-09-28 18:52:12 +0000660 modified_files = False
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000661 if isinstance(prev_entries, list):
msb@chromium.org83017012009-09-28 18:52:12 +0000662 # old .gclient_entries format was list, now dict
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000663 modified_files = gclient_scm.scm.SVN.CaptureStatus(e_dir)
msb@chromium.org83017012009-09-28 18:52:12 +0000664 else:
665 file_list = []
maruel@chromium.org75a59272010-06-11 22:34:03 +0000666 scm = gclient_scm.CreateSCM(prev_entries[entry], self.root_dir(),
msb@chromium.org83017012009-09-28 18:52:12 +0000667 entry_fixed)
668 scm.status(self._options, [], file_list)
669 modified_files = file_list != []
670 if not self._options.delete_unversioned_trees or modified_files:
maruel@chromium.orgc5e9aec2009-08-03 18:25:56 +0000671 # There are modified files in this entry. Keep warning until
672 # removed.
maruel@chromium.orgc5e9aec2009-08-03 18:25:56 +0000673 print(("\nWARNING: \"%s\" is no longer part of this client. "
674 "It is recommended that you manually remove it.\n") %
675 entry_fixed)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000676 else:
677 # Delete the entry
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000678 print("\n________ deleting \'%s\' " +
maruel@chromium.org75a59272010-06-11 22:34:03 +0000679 "in \'%s\'") % (entry_fixed, self.root_dir())
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000680 gclient_utils.RemoveDirectory(e_dir)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000681 # record the current list of entries for next time
682 self._SaveEntries(entries)
maruel@chromium.org17cdf762010-05-28 17:30:52 +0000683 return 0
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000684
685 def PrintRevInfo(self):
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000686 """Output revision info mapping for the client and its dependencies.
687
688 This allows the capture of an overall "revision" for the source tree that
maruel@chromium.org918a9ae2010-05-28 15:50:30 +0000689 can be used to reproduce the same tree in the future. It is only useful for
690 "unpinned dependencies", i.e. DEPS/deps references without a svn revision
691 number or a git hash. A git branch name isn't "pinned" since the actual
692 commit can change.
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000693
694 The --snapshot option allows creating a .gclient file to reproduce the tree.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000695 """
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000696 if not self.dependencies:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000697 raise gclient_utils.Error("No solution specified")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000698
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000699 # Inner helper to generate base url and rev tuple
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000700 def GetURLAndRev(name, original_url):
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000701 url, _ = gclient_utils.SplitUrlRevision(original_url)
maruel@chromium.org75a59272010-06-11 22:34:03 +0000702 scm = gclient_scm.CreateSCM(original_url, self.root_dir(), name)
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000703 return (url, scm.revinfo(self._options, [], None))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000704
maruel@chromium.orge3da35f2010-03-09 21:40:45 +0000705 # text of the snapshot gclient file
706 new_gclient = ""
707 # Dictionary of { path : SCM url } to ensure no duplicate solutions
708 solution_names = {}
nasser@codeaurora.orgde8f3522010-03-11 23:47:44 +0000709 entries = {}
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000710 # Run on the base solutions first.
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000711 for solution in self.dependencies:
maruel@chromium.orge3da35f2010-03-09 21:40:45 +0000712 # Dictionary of { path : SCM url } to describe the gclient checkout
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000713 name = solution.name
maruel@chromium.orge3da35f2010-03-09 21:40:45 +0000714 if name in solution_names:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000715 raise gclient_utils.Error("solution %s specified more than once" % name)
maruel@chromium.org54a07a22010-06-14 19:07:39 +0000716 (url, rev) = GetURLAndRev(name, solution.url)
msb@chromium.org770ff9e2009-09-23 17:18:18 +0000717 entries[name] = "%s@%s" % (url, rev)
maruel@chromium.orge3da35f2010-03-09 21:40:45 +0000718 solution_names[name] = "%s@%s" % (url, rev)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000719
nasser@codeaurora.orgde8f3522010-03-11 23:47:44 +0000720 # Process the dependencies next (sort alphanumerically to ensure that
721 # containing directories get populated first and for readability)
maruel@chromium.org271375b2010-06-23 19:17:38 +0000722 deps = self._ParseAllDeps(entries)
nasser@codeaurora.orgde8f3522010-03-11 23:47:44 +0000723 deps_to_process = deps.keys()
724 deps_to_process.sort()
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000725
nasser@codeaurora.orgde8f3522010-03-11 23:47:44 +0000726 # First pass for direct dependencies.
727 for d in deps_to_process:
728 if type(deps[d]) == str:
729 (url, rev) = GetURLAndRev(d, deps[d])
730 entries[d] = "%s@%s" % (url, rev)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000731
nasser@codeaurora.orgde8f3522010-03-11 23:47:44 +0000732 # Second pass for inherited deps (via the From keyword)
733 for d in deps_to_process:
tony@chromium.org4b5b1772010-04-08 01:52:56 +0000734 if isinstance(deps[d], self.FromImpl):
nasser@codeaurora.orgde8f3522010-03-11 23:47:44 +0000735 deps_parent_url = entries[deps[d].module_name]
736 if deps_parent_url.find("@") < 0:
737 raise gclient_utils.Error("From %s missing revisioned url" %
738 deps[d].module_name)
maruel@chromium.org271375b2010-06-23 19:17:38 +0000739 sub_deps_base_url = deps[deps[d].module_name]
740 sub_deps = Dependency(self, deps[d].module_name, sub_deps_base_url
741 ).ParseDepsFile(False)
742 url = deps[d].GetUrl(d, sub_deps_base_url, self.root_dir(), sub_deps)
743 (url, rev) = GetURLAndRev(d, url)
nasser@codeaurora.orgde8f3522010-03-11 23:47:44 +0000744 entries[d] = "%s@%s" % (url, rev)
maruel@chromium.orge3da35f2010-03-09 21:40:45 +0000745
nasser@codeaurora.orgde8f3522010-03-11 23:47:44 +0000746 # Build the snapshot configuration string
747 if self._options.snapshot:
748 url = entries.pop(name)
749 custom_deps = ",\n ".join(["\"%s\": \"%s\"" % (x, entries[x])
750 for x in sorted(entries.keys())])
maruel@chromium.orge3da35f2010-03-09 21:40:45 +0000751
maruel@chromium.org1f7d1182010-05-17 18:17:38 +0000752 new_gclient += self.DEFAULT_SNAPSHOT_SOLUTION_TEXT % {
nasser@codeaurora.orgde8f3522010-03-11 23:47:44 +0000753 'solution_name': name,
754 'solution_url': url,
755 'safesync_url' : "",
756 'solution_deps': custom_deps,
757 }
758 else:
759 print(";\n".join(["%s: %s" % (x, entries[x])
760 for x in sorted(entries.keys())]))
maruel@chromium.orge3da35f2010-03-09 21:40:45 +0000761
762 # Print the snapshot configuration file
763 if self._options.snapshot:
maruel@chromium.org491c04b2010-05-17 18:17:44 +0000764 config = self.DEFAULT_SNAPSHOT_FILE_TEXT % {'solution_list': new_gclient}
maruel@chromium.org75a59272010-06-11 22:34:03 +0000765 snapclient = GClient(self.root_dir(), self._options)
maruel@chromium.orge3da35f2010-03-09 21:40:45 +0000766 snapclient.SetConfig(config)
maruel@chromium.org116704f2010-06-11 17:34:38 +0000767 print(snapclient.config_content)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000768
maruel@chromium.org75a59272010-06-11 22:34:03 +0000769 def root_dir(self):
770 return self._root_dir
771
maruel@chromium.org271375b2010-06-23 19:17:38 +0000772 def enforced_os(self):
773 return self._enforced_os
774
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000775
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000776#### gclient commands.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000777
778
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000779def CMDcleanup(parser, args):
780 """Cleans up all working copies.
maruel@chromium.orgddff62d2010-05-17 21:02:36 +0000781
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000782Mostly svn-specific. Simply runs 'svn cleanup' for each module.
maruel@chromium.org79692d62010-05-14 18:57:13 +0000783"""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000784 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
785 help='override deps for the specified (comma-separated) '
786 'platform(s); \'all\' will process all deps_os '
787 'references')
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000788 (options, args) = parser.parse_args(args)
maruel@chromium.org2806acc2009-05-15 12:33:34 +0000789 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000790 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000791 raise gclient_utils.Error('client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000792 if options.verbose:
793 # Print out the .gclient file. This is longer than if we just printed the
794 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.org116704f2010-06-11 17:34:38 +0000795 print(client.config_content)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000796 return client.RunOnDeps('cleanup', args)
797
798
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000799@attr('usage', '[url] [safesync url]')
800def CMDconfig(parser, args):
maruel@chromium.orgddff62d2010-05-17 21:02:36 +0000801 """Create a .gclient file in the current directory.
802
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000803This specifies the configuration for further commands. After update/sync,
maruel@chromium.org79692d62010-05-14 18:57:13 +0000804top-level DEPS files in each module are read to determine dependent
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000805modules to operate on as well. If optional [url] parameter is
maruel@chromium.org79692d62010-05-14 18:57:13 +0000806provided, then configuration is read from a specified Subversion server
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000807URL.
maruel@chromium.org79692d62010-05-14 18:57:13 +0000808"""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000809 parser.add_option('--spec',
810 help='create a gclient file containing the provided '
811 'string. Due to Cygwin/Python brokenness, it '
812 'probably can\'t contain any newlines.')
813 parser.add_option('--name',
814 help='overrides the default name for the solution')
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000815 (options, args) = parser.parse_args(args)
maruel@chromium.org5fc2a332010-05-26 19:37:15 +0000816 if ((options.spec and args) or len(args) > 2 or
817 (not options.spec and not args)):
818 parser.error('Inconsistent arguments. Use either --spec or one or 2 args')
819
maruel@chromium.org0329e672009-05-13 18:41:04 +0000820 if os.path.exists(options.config_filename):
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000821 raise gclient_utils.Error('%s file already exists in the current directory'
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000822 % options.config_filename)
maruel@chromium.org2806acc2009-05-15 12:33:34 +0000823 client = GClient('.', options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000824 if options.spec:
825 client.SetConfig(options.spec)
826 else:
maruel@chromium.org1ab7ffc2009-06-03 17:21:37 +0000827 base_url = args[0].rstrip('/')
iposva@chromium.org8cf7a392010-04-07 17:20:26 +0000828 if not options.name:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000829 name = base_url.split('/')[-1]
iposva@chromium.org8cf7a392010-04-07 17:20:26 +0000830 else:
831 # specify an alternate relpath for the given URL.
832 name = options.name
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000833 safesync_url = ''
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000834 if len(args) > 1:
835 safesync_url = args[1]
836 client.SetDefaultConfig(name, base_url, safesync_url)
837 client.SaveConfig()
maruel@chromium.org79692d62010-05-14 18:57:13 +0000838 return 0
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000839
840
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000841def CMDexport(parser, args):
maruel@chromium.orgddff62d2010-05-17 21:02:36 +0000842 """Wrapper for svn export for all managed directories."""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000843 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
844 help='override deps for the specified (comma-separated) '
845 'platform(s); \'all\' will process all deps_os '
846 'references')
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000847 (options, args) = parser.parse_args(args)
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +0000848 if len(args) != 1:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000849 raise gclient_utils.Error('Need directory name')
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +0000850 client = GClient.LoadCurrentConfig(options)
851
852 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000853 raise gclient_utils.Error('client not configured; see \'gclient config\'')
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +0000854
855 if options.verbose:
856 # Print out the .gclient file. This is longer than if we just printed the
857 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.org116704f2010-06-11 17:34:38 +0000858 print(client.config_content)
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +0000859 return client.RunOnDeps('export', args)
860
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000861
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000862@attr('epilog', """Example:
863 gclient pack > patch.txt
864 generate simple patch for configured client and dependences
865""")
866def CMDpack(parser, args):
maruel@chromium.org79692d62010-05-14 18:57:13 +0000867 """Generate a patch which can be applied at the root of the tree.
maruel@chromium.orgddff62d2010-05-17 21:02:36 +0000868
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000869Internally, runs 'svn diff'/'git diff' on each checked out module and
maruel@chromium.org79692d62010-05-14 18:57:13 +0000870dependencies, and performs minimal postprocessing of the output. The
871resulting patch is printed to stdout and can be applied to a freshly
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000872checked out tree via 'patch -p0 < patchfile'.
maruel@chromium.org79692d62010-05-14 18:57:13 +0000873"""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000874 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
875 help='override deps for the specified (comma-separated) '
876 'platform(s); \'all\' will process all deps_os '
877 'references')
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000878 (options, args) = parser.parse_args(args)
kbr@google.comab318592009-09-04 00:54:55 +0000879 client = GClient.LoadCurrentConfig(options)
880 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000881 raise gclient_utils.Error('client not configured; see \'gclient config\'')
kbr@google.comab318592009-09-04 00:54:55 +0000882 if options.verbose:
883 # Print out the .gclient file. This is longer than if we just printed the
884 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.org116704f2010-06-11 17:34:38 +0000885 print(client.config_content)
kbr@google.comab318592009-09-04 00:54:55 +0000886 return client.RunOnDeps('pack', args)
887
888
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000889def CMDstatus(parser, args):
890 """Show modification status for every dependencies."""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000891 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
892 help='override deps for the specified (comma-separated) '
893 'platform(s); \'all\' will process all deps_os '
894 'references')
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000895 (options, args) = parser.parse_args(args)
maruel@chromium.org2806acc2009-05-15 12:33:34 +0000896 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000897 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000898 raise gclient_utils.Error('client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000899 if options.verbose:
900 # Print out the .gclient file. This is longer than if we just printed the
901 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.org116704f2010-06-11 17:34:38 +0000902 print(client.config_content)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000903 return client.RunOnDeps('status', args)
904
905
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000906@attr('epilog', """Examples:
maruel@chromium.org79692d62010-05-14 18:57:13 +0000907 gclient sync
908 update files from SCM according to current configuration,
909 *for modules which have changed since last update or sync*
910 gclient sync --force
911 update files from SCM according to current configuration, for
912 all modules (useful for recovering files deleted from local copy)
913 gclient sync --revision src@31000
914 update src directory to r31000
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000915""")
916def CMDsync(parser, args):
917 """Checkout/update all modules."""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000918 parser.add_option('-f', '--force', action='store_true',
919 help='force update even for unchanged modules')
920 parser.add_option('-n', '--nohooks', action='store_true',
921 help='don\'t run hooks after the update is complete')
922 parser.add_option('-r', '--revision', action='append',
923 dest='revisions', metavar='REV', default=[],
924 help='Enforces revision/hash for the solutions with the '
925 'format src@rev. The src@ part is optional and can be '
926 'skipped. -r can be used multiple times when .gclient '
927 'has multiple solutions configured and will work even '
928 'if the src@ part is skipped.')
929 parser.add_option('-H', '--head', action='store_true',
930 help='skips any safesync_urls specified in '
931 'configured solutions and sync to head instead')
932 parser.add_option('-D', '--delete_unversioned_trees', action='store_true',
933 help='delete any unexpected unversioned trees '
934 'that are in the checkout')
935 parser.add_option('-R', '--reset', action='store_true',
936 help='resets any local changes before updating (git only)')
937 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
938 help='override deps for the specified (comma-separated) '
939 'platform(s); \'all\' will process all deps_os '
940 'references')
941 parser.add_option('-m', '--manually_grab_svn_rev', action='store_true',
942 help='Skip svn up whenever possible by requesting '
943 'actual HEAD revision from the repository')
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000944 (options, args) = parser.parse_args(args)
maruel@chromium.org2806acc2009-05-15 12:33:34 +0000945 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000946
947 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000948 raise gclient_utils.Error('client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000949
maruel@chromium.org307d1792010-05-31 20:03:13 +0000950 if options.revisions and options.head:
951 # TODO(maruel): Make it a parser.error if it doesn't break any builder.
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000952 print('Warning: you cannot use both --head and --revision')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000953
954 if options.verbose:
955 # Print out the .gclient file. This is longer than if we just printed the
956 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.org116704f2010-06-11 17:34:38 +0000957 print(client.config_content)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000958 return client.RunOnDeps('update', args)
959
960
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000961def CMDupdate(parser, args):
maruel@chromium.orgddff62d2010-05-17 21:02:36 +0000962 """Alias for the sync command. Deprecated."""
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000963 return CMDsync(parser, args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000964
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000965def CMDdiff(parser, args):
966 """Displays local diff for every dependencies."""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000967 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
968 help='override deps for the specified (comma-separated) '
969 'platform(s); \'all\' will process all deps_os '
970 'references')
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000971 (options, args) = parser.parse_args(args)
maruel@chromium.org2806acc2009-05-15 12:33:34 +0000972 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000973 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000974 raise gclient_utils.Error('client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000975 if options.verbose:
976 # Print out the .gclient file. This is longer than if we just printed the
977 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.org116704f2010-06-11 17:34:38 +0000978 print(client.config_content)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000979 return client.RunOnDeps('diff', args)
980
981
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000982def CMDrevert(parser, args):
983 """Revert all modifications in every dependencies."""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000984 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
985 help='override deps for the specified (comma-separated) '
986 'platform(s); \'all\' will process all deps_os '
987 'references')
988 parser.add_option('-n', '--nohooks', action='store_true',
989 help='don\'t run hooks after the revert is complete')
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000990 (options, args) = parser.parse_args(args)
991 # --force is implied.
992 options.force = True
maruel@chromium.org2806acc2009-05-15 12:33:34 +0000993 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000994 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +0000995 raise gclient_utils.Error('client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000996 return client.RunOnDeps('revert', args)
997
998
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000999def CMDrunhooks(parser, args):
1000 """Runs hooks for files that have been modified in the local working copy."""
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00001001 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
1002 help='override deps for the specified (comma-separated) '
1003 'platform(s); \'all\' will process all deps_os '
1004 'references')
1005 parser.add_option('-f', '--force', action='store_true', default=True,
1006 help='Deprecated. No effect.')
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001007 (options, args) = parser.parse_args(args)
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001008 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001009 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00001010 raise gclient_utils.Error('client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001011 if options.verbose:
1012 # Print out the .gclient file. This is longer than if we just printed the
1013 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.org116704f2010-06-11 17:34:38 +00001014 print(client.config_content)
maruel@chromium.org5df6a462009-08-28 18:52:26 +00001015 options.force = True
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001016 options.nohooks = False
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001017 return client.RunOnDeps('runhooks', args)
1018
1019
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001020def CMDrevinfo(parser, args):
maruel@chromium.org9eda4112010-06-11 18:56:10 +00001021 """Output revision info mapping for the client and its dependencies.
1022
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00001023 This allows the capture of an overall 'revision' for the source tree that
maruel@chromium.org9eda4112010-06-11 18:56:10 +00001024 can be used to reproduce the same tree in the future. It is only useful for
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00001025 'unpinned dependencies', i.e. DEPS/deps references without a svn revision
1026 number or a git hash. A git branch name isn't 'pinned' since the actual
maruel@chromium.org9eda4112010-06-11 18:56:10 +00001027 commit can change.
1028 """
1029 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
1030 help='override deps for the specified (comma-separated) '
1031 'platform(s); \'all\' will process all deps_os '
1032 'references')
1033 parser.add_option('-s', '--snapshot', action='store_true',
1034 help='creates a snapshot .gclient file of the current '
1035 'version of all repositories to reproduce the tree, '
1036 'implies -a')
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001037 (options, args) = parser.parse_args(args)
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001038 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001039 if not client:
maruel@chromium.org0b6a0842010-06-15 14:34:19 +00001040 raise gclient_utils.Error('client not configured; see \'gclient config\'')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001041 client.PrintRevInfo()
maruel@chromium.org79692d62010-05-14 18:57:13 +00001042 return 0
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001043
1044
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001045def Command(name):
1046 return getattr(sys.modules[__name__], 'CMD' + name, None)
1047
1048
1049def CMDhelp(parser, args):
1050 """Prints list of commands or help for a specific command."""
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001051 (_, args) = parser.parse_args(args)
maruel@chromium.orgddff62d2010-05-17 21:02:36 +00001052 if len(args) == 1:
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001053 return Main(args + ['--help'])
maruel@chromium.orgddff62d2010-05-17 21:02:36 +00001054 parser.print_help()
1055 return 0
1056
1057
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001058def GenUsage(parser, command):
1059 """Modify an OptParse object with the function's documentation."""
1060 obj = Command(command)
1061 if command == 'help':
1062 command = '<command>'
1063 # OptParser.description prefer nicely non-formatted strings.
1064 parser.description = re.sub('[\r\n ]{2,}', ' ', obj.__doc__)
1065 usage = getattr(obj, 'usage', '')
1066 parser.set_usage('%%prog %s [options] %s' % (command, usage))
1067 parser.epilog = getattr(obj, 'epilog', None)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001068
1069
1070def Main(argv):
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001071 """Doesn't parse the arguments here, just find the right subcommand to
1072 execute."""
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001073 try:
1074 # Do it late so all commands are listed.
1075 CMDhelp.usage = ('\n\nCommands are:\n' + '\n'.join([
1076 ' %-10s %s' % (fn[3:], Command(fn[3:]).__doc__.split('\n')[0].strip())
1077 for fn in dir(sys.modules[__name__]) if fn.startswith('CMD')]))
1078 parser = optparse.OptionParser(version='%prog ' + __version__)
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001079 parser.add_option('-v', '--verbose', action='count', default=0,
1080 help='Produces additional output for diagnostics. Can be '
1081 'used up to three times for more logging info.')
1082 parser.add_option('--gclientfile', dest='config_filename',
1083 default=os.environ.get('GCLIENT_FILE', '.gclient'),
1084 help='Specify an alternate %default file')
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001085 # Integrate standard options processing.
1086 old_parser = parser.parse_args
1087 def Parse(args):
1088 (options, args) = old_parser(args)
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001089 level = None
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001090 if options.verbose == 2:
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001091 level = logging.INFO
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001092 elif options.verbose > 2:
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001093 level = logging.DEBUG
1094 logging.basicConfig(level=level,
1095 format='%(module)s(%(lineno)d) %(funcName)s:%(message)s')
1096 options.entries_filename = options.config_filename + '_entries'
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001097 if not hasattr(options, 'revisions'):
1098 # GClient.RunOnDeps expects it even if not applicable.
1099 options.revisions = []
1100 if not hasattr(options, 'head'):
1101 options.head = None
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001102 if not hasattr(options, 'nohooks'):
1103 options.nohooks = True
1104 if not hasattr(options, 'deps_os'):
1105 options.deps_os = None
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001106 return (options, args)
1107 parser.parse_args = Parse
1108 # We don't want wordwrapping in epilog (usually examples)
1109 parser.format_epilog = lambda _: parser.epilog or ''
1110 if argv:
1111 command = Command(argv[0])
1112 if command:
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001113 # 'fix' the usage and the description now that we know the subcommand.
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001114 GenUsage(parser, argv[0])
1115 return command(parser, argv[1:])
1116 # Not a known command. Default to help.
1117 GenUsage(parser, 'help')
1118 return CMDhelp(parser, argv)
1119 except gclient_utils.Error, e:
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001120 print >> sys.stderr, 'Error: %s' % str(e)
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001121 return 1
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001122
1123
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001124if '__main__' == __name__:
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001125 sys.exit(Main(sys.argv[1:]))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001126
1127# vim: ts=2:sw=2:tw=80:et: