blob: 8704be333f4d5ee2f828b1fbe45c5ba4c96372b9 [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
6"""A wrapper script to manage a set of client modules in different SCM.
7
8This script is intended to be used to help basic management of client
msb@chromium.orgd6504212010-01-13 17:34:31 +00009program sources residing in one or more Subversion modules and Git
10repositories, along with other modules it depends on, also in Subversion or Git,
11but possibly on multiple respositories, making a wrapper system apparently
12necessary.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000013
14Files
15 .gclient : Current client configuration, written by 'config' command.
16 Format is a Python script defining 'solutions', a list whose
17 entries each are maps binding the strings "name" and "url"
18 to strings specifying the name and location of the client
19 module, as well as "custom_deps" to a map similar to the DEPS
20 file below.
21 .gclient_entries : A cache constructed by 'update' command. Format is a
22 Python script defining 'entries', a list of the names
23 of all modules in the client
24 <module>/DEPS : Python script defining var 'deps' as a map from each requisite
25 submodule name to a URL where it can be found (via one SCM)
26
27Hooks
28 .gclient and DEPS files may optionally contain a list named "hooks" to
29 allow custom actions to be performed based on files that have changed in the
evan@chromium.org67820ef2009-07-27 17:23:00 +000030 working copy as a result of a "sync"/"update" or "revert" operation. This
31 could be prevented by using --nohooks (hooks run by default). Hooks can also
maruel@chromium.org5df6a462009-08-28 18:52:26 +000032 be forced to run with the "runhooks" operation. If "sync" is run with
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000033 --force, all known hooks will run regardless of the state of the working
34 copy.
35
36 Each item in a "hooks" list is a dict, containing these two keys:
37 "pattern" The associated value is a string containing a regular
38 expression. When a file whose pathname matches the expression
39 is checked out, updated, or reverted, the hook's "action" will
40 run.
41 "action" A list describing a command to run along with its arguments, if
42 any. An action command will run at most one time per gclient
43 invocation, regardless of how many files matched the pattern.
44 The action is executed in the same directory as the .gclient
45 file. If the first item in the list is the string "python",
46 the current Python interpreter (sys.executable) will be used
phajdan.jr@chromium.org71b40682009-07-31 23:40:09 +000047 to run the command. If the list contains string "$matching_files"
48 it will be removed from the list and the list will be extended
49 by the list of matching files.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000050
51 Example:
52 hooks = [
53 { "pattern": "\\.(gif|jpe?g|pr0n|png)$",
54 "action": ["python", "image_indexer.py", "--all"]},
55 ]
56"""
57
maruel@chromium.org096469f2010-05-26 13:34:30 +000058__version__ = "0.4"
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000059
60import errno
maruel@chromium.org754960e2009-09-21 12:31:05 +000061import logging
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000062import optparse
63import os
msb@chromium.org2e38de72009-09-28 17:04:47 +000064import pprint
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000065import re
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000066import sys
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000067import urlparse
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000068import urllib
69
maruel@chromium.orgada4c652009-12-03 15:32:01 +000070import breakpad
71
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000072import gclient_scm
73import gclient_utils
nasser@codeaurora.org1f7a3d12010-02-04 15:11:50 +000074from third_party.repo.progress import Progress
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000075
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000076
maruel@chromium.org1f7d1182010-05-17 18:17:38 +000077def attr(attr, data):
78 """Sets an attribute on a function."""
79 def hook(fn):
80 setattr(fn, attr, data)
81 return fn
82 return hook
maruel@chromium.orge3da35f2010-03-09 21:40:45 +000083
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000084
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000085## GClient implementation.
86
87
88class GClient(object):
89 """Object that represent a gclient checkout."""
90
91 supported_commands = [
kbr@google.comab318592009-09-04 00:54:55 +000092 'cleanup', 'diff', 'export', 'pack', 'revert', 'status', 'update',
93 'runhooks'
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000094 ]
95
maruel@chromium.org491c04b2010-05-17 18:17:44 +000096 deps_os_choices = {
97 "win32": "win",
98 "win": "win",
99 "cygwin": "win",
100 "darwin": "mac",
101 "mac": "mac",
102 "unix": "unix",
103 "linux": "unix",
104 "linux2": "unix",
105 }
106
maruel@chromium.org1f7d1182010-05-17 18:17:38 +0000107 DEPS_FILE = 'DEPS'
108
109 DEFAULT_CLIENT_FILE_TEXT = ("""\
110solutions = [
111 { "name" : "%(solution_name)s",
112 "url" : "%(solution_url)s",
113 "custom_deps" : {
114 },
115 "safesync_url": "%(safesync_url)s"
116 },
117]
118""")
119
120 DEFAULT_SNAPSHOT_SOLUTION_TEXT = ("""\
121 { "name" : "%(solution_name)s",
122 "url" : "%(solution_url)s",
123 "custom_deps" : {
124 %(solution_deps)s,
125 },
126 "safesync_url": "%(safesync_url)s"
127 },
128""")
129
130 DEFAULT_SNAPSHOT_FILE_TEXT = ("""\
131# Snapshot generated with gclient revinfo --snapshot
132solutions = [
133%(solution_list)s
134]
135""")
136
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000137 def __init__(self, root_dir, options):
138 self._root_dir = root_dir
139 self._options = options
140 self._config_content = None
141 self._config_dict = {}
142 self._deps_hooks = []
143
144 def SetConfig(self, content):
145 self._config_dict = {}
146 self._config_content = content
skylined@chromium.orgdf0032c2009-05-29 10:43:56 +0000147 try:
148 exec(content, self._config_dict)
149 except SyntaxError, e:
150 try:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000151 __pychecker__ = 'no-objattrs'
skylined@chromium.orgdf0032c2009-05-29 10:43:56 +0000152 # Try to construct a human readable error message
153 error_message = [
154 'There is a syntax error in your configuration file.',
155 'Line #%s, character %s:' % (e.lineno, e.offset),
156 '"%s"' % re.sub(r'[\r\n]*$', '', e.text) ]
157 except:
158 # Something went wrong, re-raise the original exception
159 raise e
160 else:
161 # Raise a new exception with the human readable message:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000162 raise gclient_utils.Error('\n'.join(error_message))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000163
164 def SaveConfig(self):
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000165 gclient_utils.FileWrite(os.path.join(self._root_dir,
166 self._options.config_filename),
167 self._config_content)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000168
169 def _LoadConfig(self):
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000170 client_source = gclient_utils.FileRead(
171 os.path.join(self._root_dir, self._options.config_filename))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000172 self.SetConfig(client_source)
173
174 def ConfigContent(self):
175 return self._config_content
176
177 def GetVar(self, key, default=None):
178 return self._config_dict.get(key, default)
179
180 @staticmethod
181 def LoadCurrentConfig(options, from_dir=None):
182 """Searches for and loads a .gclient file relative to the current working
183 dir.
184
185 Returns:
186 A dict representing the contents of the .gclient file or an empty dict if
187 the .gclient file doesn't exist.
188 """
189 if not from_dir:
190 from_dir = os.curdir
191 path = os.path.realpath(from_dir)
maruel@chromium.org0329e672009-05-13 18:41:04 +0000192 while not os.path.exists(os.path.join(path, options.config_filename)):
maruel@chromium.org55e724e2010-03-11 19:36:49 +0000193 split_path = os.path.split(path)
194 if not split_path[1]:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000195 return None
maruel@chromium.org55e724e2010-03-11 19:36:49 +0000196 path = split_path[0]
maruel@chromium.org2806acc2009-05-15 12:33:34 +0000197 client = GClient(path, options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000198 client._LoadConfig()
199 return client
200
201 def SetDefaultConfig(self, solution_name, solution_url, safesync_url):
maruel@chromium.org1f7d1182010-05-17 18:17:38 +0000202 self.SetConfig(self.DEFAULT_CLIENT_FILE_TEXT % {
gspencer@google.comdf2d5902009-09-11 22:16:21 +0000203 'solution_name': solution_name,
204 'solution_url': solution_url,
205 'safesync_url' : safesync_url,
206 })
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000207
208 def _SaveEntries(self, entries):
209 """Creates a .gclient_entries file to record the list of unique checkouts.
210
211 The .gclient_entries file lives in the same directory as .gclient.
212
213 Args:
214 entries: A sequence of solution names.
215 """
maruel@chromium.orge41f6822010-04-08 16:37:06 +0000216 # Sometimes pprint.pformat will use {', sometimes it'll use { ' ... It
217 # makes testing a bit too fun.
218 result = pprint.pformat(entries, 2)
219 if result.startswith('{\''):
maruel@chromium.org1edec4d2010-04-08 17:17:06 +0000220 result = '{ \'' + result[2:]
maruel@chromium.orge41f6822010-04-08 16:37:06 +0000221 text = "entries = \\\n" + result + '\n'
msb@chromium.org2e38de72009-09-28 17:04:47 +0000222 file_path = os.path.join(self._root_dir, self._options.entries_filename)
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000223 gclient_utils.FileWrite(file_path, text)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000224
225 def _ReadEntries(self):
226 """Read the .gclient_entries file for the given client.
227
228 Args:
229 client: The client for which the entries file should be read.
230
231 Returns:
232 A sequence of solution names, which will be empty if there is the
233 entries file hasn't been created yet.
234 """
235 scope = {}
236 filename = os.path.join(self._root_dir, self._options.entries_filename)
maruel@chromium.org0329e672009-05-13 18:41:04 +0000237 if not os.path.exists(filename):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000238 return []
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000239 exec(gclient_utils.FileRead(filename), scope)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000240 return scope["entries"]
241
242 class FromImpl:
243 """Used to implement the From syntax."""
244
tony@chromium.org30ef9ae2010-04-09 02:18:05 +0000245 def __init__(self, module_name, sub_target_name=None):
246 """module_name is the dep module we want to include from. It can also be
247 the name of a subdirectory to include from.
248
249 sub_target_name is an optional parameter if the module name in the other
250 DEPS file is different. E.g., you might want to map src/net to net."""
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000251 self.module_name = module_name
tony@chromium.org30ef9ae2010-04-09 02:18:05 +0000252 self.sub_target_name = sub_target_name
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000253
254 def __str__(self):
tony@chromium.org30ef9ae2010-04-09 02:18:05 +0000255 return 'From(%s, %s)' % (repr(self.module_name),
256 repr(self.sub_target_name))
257
258 def GetUrl(self, target_name, sub_deps_base_url, root_dir, sub_deps):
259 """Resolve the URL for this From entry."""
260 sub_deps_target_name = target_name
261 if self.sub_target_name:
262 sub_deps_target_name = self.sub_target_name
263 url = sub_deps[sub_deps_target_name]
264 if url.startswith('/'):
265 # If it's a relative URL, we need to resolve the URL relative to the
266 # sub deps base URL.
267 if not isinstance(sub_deps_base_url, basestring):
268 sub_deps_base_url = sub_deps_base_url.GetPath()
269 scm = gclient_scm.CreateSCM(sub_deps_base_url, root_dir,
270 None)
271 url = scm.FullUrlForRelativeUrl(url)
272 return url
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000273
tony@chromium.org4b5b1772010-04-08 01:52:56 +0000274 class FileImpl:
275 """Used to implement the File('') syntax which lets you sync a single file
276 from an SVN repo."""
277
278 def __init__(self, file_location):
279 self.file_location = file_location
280
281 def __str__(self):
282 return 'File("%s")' % self.file_location
283
284 def GetPath(self):
285 return os.path.split(self.file_location)[0]
286
287 def GetFilename(self):
288 rev_tokens = self.file_location.split('@')
289 return os.path.split(rev_tokens[0])[1]
290
291 def GetRevision(self):
292 rev_tokens = self.file_location.split('@')
293 if len(rev_tokens) > 1:
294 return rev_tokens[1]
295 return None
296
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000297 class _VarImpl:
298 def __init__(self, custom_vars, local_scope):
299 self._custom_vars = custom_vars
300 self._local_scope = local_scope
301
302 def Lookup(self, var_name):
303 """Implements the Var syntax."""
304 if var_name in self._custom_vars:
305 return self._custom_vars[var_name]
306 elif var_name in self._local_scope.get("vars", {}):
307 return self._local_scope["vars"][var_name]
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000308 raise gclient_utils.Error("Var is not defined: %s" % var_name)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000309
310 def _ParseSolutionDeps(self, solution_name, solution_deps_content,
tony@chromium.org30ef9ae2010-04-09 02:18:05 +0000311 custom_vars, parse_hooks):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000312 """Parses the DEPS file for the specified solution.
313
314 Args:
315 solution_name: The name of the solution to query.
316 solution_deps_content: Content of the DEPS file for the solution
317 custom_vars: A dict of vars to override any vars defined in the DEPS file.
318
319 Returns:
320 A dict mapping module names (as relative paths) to URLs or an empty
321 dict if the solution does not have a DEPS file.
322 """
323 # Skip empty
324 if not solution_deps_content:
325 return {}
326 # Eval the content
327 local_scope = {}
328 var = self._VarImpl(custom_vars, local_scope)
tony@chromium.org4b5b1772010-04-08 01:52:56 +0000329 global_scope = {
330 "File": self.FileImpl,
331 "From": self.FromImpl,
332 "Var": var.Lookup,
333 "deps_os": {},
334 }
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000335 exec(solution_deps_content, global_scope, local_scope)
336 deps = local_scope.get("deps", {})
337
338 # load os specific dependencies if defined. these dependencies may
339 # override or extend the values defined by the 'deps' member.
340 if "deps_os" in local_scope:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000341
342 if self._options.deps_os is not None:
343 deps_to_include = self._options.deps_os.split(",")
344 if "all" in deps_to_include:
maruel@chromium.org491c04b2010-05-17 18:17:44 +0000345 deps_to_include = list(set(self.deps_os_choices.itervalues()))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000346 else:
maruel@chromium.org491c04b2010-05-17 18:17:44 +0000347 deps_to_include = [self.deps_os_choices.get(sys.platform, "unix")]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000348
349 deps_to_include = set(deps_to_include)
350 for deps_os_key in deps_to_include:
351 os_deps = local_scope["deps_os"].get(deps_os_key, {})
352 if len(deps_to_include) > 1:
353 # Ignore any overrides when including deps for more than one
354 # platform, so we collect the broadest set of dependencies available.
355 # We may end up with the wrong revision of something for our
356 # platform, but this is the best we can do.
357 deps.update([x for x in os_deps.items() if not x[0] in deps])
358 else:
359 deps.update(os_deps)
360
tony@chromium.org30ef9ae2010-04-09 02:18:05 +0000361 if 'hooks' in local_scope and parse_hooks:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000362 self._deps_hooks.extend(local_scope['hooks'])
363
364 # If use_relative_paths is set in the DEPS file, regenerate
365 # the dictionary using paths relative to the directory containing
366 # the DEPS file.
367 if local_scope.get('use_relative_paths'):
368 rel_deps = {}
369 for d, url in deps.items():
370 # normpath is required to allow DEPS to use .. in their
371 # dependency local path.
372 rel_deps[os.path.normpath(os.path.join(solution_name, d))] = url
373 return rel_deps
374 else:
375 return deps
376
377 def _ParseAllDeps(self, solution_urls, solution_deps_content):
378 """Parse the complete list of dependencies for the client.
379
380 Args:
381 solution_urls: A dict mapping module names (as relative paths) to URLs
382 corresponding to the solutions specified by the client. This parameter
383 is passed as an optimization.
384 solution_deps_content: A dict mapping module names to the content
385 of their DEPS files
386
387 Returns:
388 A dict mapping module names (as relative paths) to URLs corresponding
389 to the entire set of dependencies to checkout for the given client.
390
391 Raises:
392 Error: If a dependency conflicts with another dependency or of a solution.
393 """
394 deps = {}
395 for solution in self.GetVar("solutions"):
396 custom_vars = solution.get("custom_vars", {})
397 solution_deps = self._ParseSolutionDeps(
398 solution["name"],
399 solution_deps_content[solution["name"]],
tony@chromium.org30ef9ae2010-04-09 02:18:05 +0000400 custom_vars,
401 True)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000402
403 # If a line is in custom_deps, but not in the solution, we want to append
404 # this line to the solution.
405 if "custom_deps" in solution:
406 for d in solution["custom_deps"]:
407 if d not in solution_deps:
408 solution_deps[d] = solution["custom_deps"][d]
409
410 for d in solution_deps:
411 if "custom_deps" in solution and d in solution["custom_deps"]:
412 # Dependency is overriden.
413 url = solution["custom_deps"][d]
414 if url is None:
415 continue
416 else:
417 url = solution_deps[d]
418 # if we have a From reference dependent on another solution, then
419 # just skip the From reference. When we pull deps for the solution,
420 # we will take care of this dependency.
421 #
422 # If multiple solutions all have the same From reference, then we
423 # should only add one to our list of dependencies.
tony@chromium.org4b5b1772010-04-08 01:52:56 +0000424 if isinstance(url, self.FromImpl):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000425 if url.module_name in solution_urls:
426 # Already parsed.
427 continue
428 if d in deps and type(deps[d]) != str:
429 if url.module_name == deps[d].module_name:
430 continue
tony@chromium.org4b5b1772010-04-08 01:52:56 +0000431 elif isinstance(url, str):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000432 parsed_url = urlparse.urlparse(url)
433 scheme = parsed_url[0]
434 if not scheme:
435 # A relative url. Fetch the real base.
436 path = parsed_url[2]
437 if path[0] != "/":
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000438 raise gclient_utils.Error(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000439 "relative DEPS entry \"%s\" must begin with a slash" % d)
msb@chromium.orge6f78352010-01-13 17:05:33 +0000440 # Create a scm just to query the full url.
441 scm = gclient_scm.CreateSCM(solution["url"], self._root_dir,
442 None)
443 url = scm.FullUrlForRelativeUrl(url)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000444 if d in deps and deps[d] != url:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000445 raise gclient_utils.Error(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000446 "Solutions have conflicting versions of dependency \"%s\"" % d)
447 if d in solution_urls and solution_urls[d] != url:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000448 raise gclient_utils.Error(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000449 "Dependency \"%s\" conflicts with specified solution" % d)
450 # Grab the dependency.
451 deps[d] = url
452 return deps
453
phajdan.jr@chromium.org71b40682009-07-31 23:40:09 +0000454 def _RunHookAction(self, hook_dict, matching_file_list):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000455 """Runs the action from a single hook.
456 """
457 command = hook_dict['action'][:]
458 if command[0] == 'python':
459 # If the hook specified "python" as the first item, the action is a
460 # Python script. Run it by starting a new copy of the same
461 # interpreter.
462 command[0] = sys.executable
463
phajdan.jr@chromium.org71b40682009-07-31 23:40:09 +0000464 if '$matching_files' in command:
phajdan.jr@chromium.org68f2e092009-08-06 17:05:35 +0000465 splice_index = command.index('$matching_files')
466 command[splice_index:splice_index + 1] = matching_file_list
phajdan.jr@chromium.org71b40682009-07-31 23:40:09 +0000467
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000468 # Use a discrete exit status code of 2 to indicate that a hook action
469 # failed. Users of this script may wish to treat hook action failures
470 # differently from VC failures.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000471 gclient_utils.SubprocessCall(command, self._root_dir, fail_status=2)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000472
473 def _RunHooks(self, command, file_list, is_using_git):
474 """Evaluates all hooks, running actions as needed.
475 """
476 # Hooks only run for these command types.
477 if not command in ('update', 'revert', 'runhooks'):
478 return
479
evan@chromium.org67820ef2009-07-27 17:23:00 +0000480 # Hooks only run when --nohooks is not specified
481 if self._options.nohooks:
482 return
483
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000484 # Get any hooks from the .gclient file.
485 hooks = self.GetVar("hooks", [])
486 # Add any hooks found in DEPS files.
487 hooks.extend(self._deps_hooks)
488
489 # If "--force" was specified, run all hooks regardless of what files have
490 # changed. If the user is using git, then we don't know what files have
491 # changed so we always run all hooks.
492 if self._options.force or is_using_git:
493 for hook_dict in hooks:
phajdan.jr@chromium.org71b40682009-07-31 23:40:09 +0000494 self._RunHookAction(hook_dict, [])
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000495 return
496
497 # Run hooks on the basis of whether the files from the gclient operation
498 # match each hook's pattern.
499 for hook_dict in hooks:
500 pattern = re.compile(hook_dict['pattern'])
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000501 matching_file_list = [f for f in file_list if pattern.search(f)]
phajdan.jr@chromium.org71b40682009-07-31 23:40:09 +0000502 if matching_file_list:
503 self._RunHookAction(hook_dict, matching_file_list)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000504
505 def RunOnDeps(self, command, args):
506 """Runs a command on each dependency in a client and its dependencies.
507
508 The module's dependencies are specified in its top-level DEPS files.
509
510 Args:
511 command: The command to use (e.g., 'status' or 'diff')
512 args: list of str - extra arguments to add to the command line.
513
514 Raises:
515 Error: If the client has conflicting entries.
516 """
517 if not command in self.supported_commands:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000518 raise gclient_utils.Error("'%s' is an unsupported command" % command)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000519
520 # Check for revision overrides.
521 revision_overrides = {}
522 for revision in self._options.revisions:
523 if revision.find("@") == -1:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000524 raise gclient_utils.Error(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000525 "Specify the full dependency when specifying a revision number.")
526 revision_elem = revision.split("@")
527 # Disallow conflicting revs
528 if revision_overrides.has_key(revision_elem[0]) and \
529 revision_overrides[revision_elem[0]] != revision_elem[1]:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000530 raise gclient_utils.Error(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000531 "Conflicting revision numbers specified.")
532 revision_overrides[revision_elem[0]] = revision_elem[1]
533
534 solutions = self.GetVar("solutions")
535 if not solutions:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000536 raise gclient_utils.Error("No solution specified")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000537
538 # When running runhooks --force, there's no need to consult the SCM.
539 # All known hooks are expected to run unconditionally regardless of working
540 # copy state, so skip the SCM status check.
541 run_scm = not (command == 'runhooks' and self._options.force)
542
543 entries = {}
544 entries_deps_content = {}
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000545 file_list = []
546 # Run on the base solutions first.
547 for solution in solutions:
548 name = solution["name"]
maruel@chromium.org1f7d1182010-05-17 18:17:38 +0000549 deps_file = solution.get("deps_file", self.DEPS_FILE)
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000550 if '/' in deps_file or '\\' in deps_file:
551 raise gclient_utils.Error('deps_file name must not be a path, just a '
552 'filename.')
553 if name in entries:
554 raise gclient_utils.Error("solution %s specified more than once" % name)
555 url = solution["url"]
556 entries[name] = url
557 if run_scm and url:
558 self._options.revision = revision_overrides.get(name)
559 scm = gclient_scm.CreateSCM(url, self._root_dir, name)
560 scm.RunCommand(command, self._options, args, file_list)
561 file_list = [os.path.join(name, f.strip()) for f in file_list]
562 self._options.revision = None
563 try:
564 deps_content = gclient_utils.FileRead(
565 os.path.join(self._root_dir, name, deps_file))
566 except IOError, e:
567 if e.errno != errno.ENOENT:
568 raise
569 deps_content = ""
570 entries_deps_content[name] = deps_content
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000571
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000572 # Process the dependencies next (sort alphanumerically to ensure that
573 # containing directories get populated first and for readability)
574 deps = self._ParseAllDeps(entries, entries_deps_content)
575 deps_to_process = deps.keys()
576 deps_to_process.sort()
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000577
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000578 # First pass for direct dependencies.
579 if command == 'update' and not self._options.verbose:
580 pm = Progress('Syncing projects', len(deps_to_process))
581 for d in deps_to_process:
nasser@codeaurora.org1f7a3d12010-02-04 15:11:50 +0000582 if command == 'update' and not self._options.verbose:
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000583 pm.update()
584 if type(deps[d]) == str:
585 url = deps[d]
586 entries[d] = url
587 if run_scm:
588 self._options.revision = revision_overrides.get(d)
589 scm = gclient_scm.CreateSCM(url, self._root_dir, d)
590 scm.RunCommand(command, self._options, args, file_list)
591 self._options.revision = None
592 elif isinstance(deps[d], self.FileImpl):
maruel@chromium.org491c04b2010-05-17 18:17:44 +0000593 file_dep = deps[d]
594 self._options.revision = file_dep.GetRevision()
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000595 if run_scm:
maruel@chromium.org491c04b2010-05-17 18:17:44 +0000596 scm = gclient_scm.CreateSCM(file_dep.GetPath(), self._root_dir, d)
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000597 scm.RunCommand("updatesingle", self._options,
maruel@chromium.org491c04b2010-05-17 18:17:44 +0000598 args + [file_dep.GetFilename()], file_list)
maruel@chromium.org79692d62010-05-14 18:57:13 +0000599
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000600 if command == 'update' and not self._options.verbose:
601 pm.end()
piman@chromium.org6f363722010-04-27 00:41:09 +0000602
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000603 # Second pass for inherited deps (via the From keyword)
604 for d in deps_to_process:
605 if isinstance(deps[d], self.FromImpl):
606 filename = os.path.join(self._root_dir,
607 deps[d].module_name,
maruel@chromium.org1f7d1182010-05-17 18:17:38 +0000608 self.DEPS_FILE)
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000609 content = gclient_utils.FileRead(filename)
610 sub_deps = self._ParseSolutionDeps(deps[d].module_name, content, {},
611 False)
612 # Getting the URL from the sub_deps file can involve having to resolve
613 # a File() or having to resolve a relative URL. To resolve relative
614 # URLs, we need to pass in the orignal sub deps URL.
615 sub_deps_base_url = deps[deps[d].module_name]
616 url = deps[d].GetUrl(d, sub_deps_base_url, self._root_dir, sub_deps)
617 entries[d] = url
618 if run_scm:
619 self._options.revision = revision_overrides.get(d)
620 scm = gclient_scm.CreateSCM(url, self._root_dir, d)
621 scm.RunCommand(command, self._options, args, file_list)
622 self._options.revision = None
gspencer@google.comdf2d5902009-09-11 22:16:21 +0000623
phajdan.jr@chromium.orgd83b2b22009-08-11 15:30:55 +0000624 # Convert all absolute paths to relative.
625 for i in range(len(file_list)):
626 # TODO(phajdan.jr): We should know exactly when the paths are absolute.
627 # It depends on the command being executed (like runhooks vs sync).
628 if not os.path.isabs(file_list[i]):
629 continue
630
631 prefix = os.path.commonprefix([self._root_dir.lower(),
632 file_list[i].lower()])
633 file_list[i] = file_list[i][len(prefix):]
634
635 # Strip any leading path separators.
636 while file_list[i].startswith('\\') or file_list[i].startswith('/'):
637 file_list[i] = file_list[i][1:]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000638
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000639 is_using_git = gclient_utils.IsUsingGit(self._root_dir, entries.keys())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000640 self._RunHooks(command, file_list, is_using_git)
641
642 if command == 'update':
ajwong@chromium.orgcdcee802009-06-23 15:30:42 +0000643 # Notify the user if there is an orphaned entry in their working copy.
644 # Only delete the directory if there are no changes in it, and
645 # delete_unversioned_trees is set to true.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000646 prev_entries = self._ReadEntries()
647 for entry in prev_entries:
maruel@chromium.orgc5e9aec2009-08-03 18:25:56 +0000648 # Fix path separator on Windows.
649 entry_fixed = entry.replace('/', os.path.sep)
650 e_dir = os.path.join(self._root_dir, entry_fixed)
651 # Use entry and not entry_fixed there.
maruel@chromium.org0329e672009-05-13 18:41:04 +0000652 if entry not in entries and os.path.exists(e_dir):
msb@chromium.org83017012009-09-28 18:52:12 +0000653 modified_files = False
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000654 if isinstance(prev_entries, list):
msb@chromium.org83017012009-09-28 18:52:12 +0000655 # old .gclient_entries format was list, now dict
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000656 modified_files = gclient_scm.scm.SVN.CaptureStatus(e_dir)
msb@chromium.org83017012009-09-28 18:52:12 +0000657 else:
658 file_list = []
659 scm = gclient_scm.CreateSCM(prev_entries[entry], self._root_dir,
660 entry_fixed)
661 scm.status(self._options, [], file_list)
662 modified_files = file_list != []
663 if not self._options.delete_unversioned_trees or modified_files:
maruel@chromium.orgc5e9aec2009-08-03 18:25:56 +0000664 # There are modified files in this entry. Keep warning until
665 # removed.
maruel@chromium.orgc5e9aec2009-08-03 18:25:56 +0000666 print(("\nWARNING: \"%s\" is no longer part of this client. "
667 "It is recommended that you manually remove it.\n") %
668 entry_fixed)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000669 else:
670 # Delete the entry
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000671 print("\n________ deleting \'%s\' " +
maruel@chromium.orgc5e9aec2009-08-03 18:25:56 +0000672 "in \'%s\'") % (entry_fixed, self._root_dir)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000673 gclient_utils.RemoveDirectory(e_dir)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000674 # record the current list of entries for next time
675 self._SaveEntries(entries)
676
677 def PrintRevInfo(self):
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000678 """Output revision info mapping for the client and its dependencies.
679
680 This allows the capture of an overall "revision" for the source tree that
681 can be used to reproduce the same tree in the future. The actual output
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000682 contains enough information (source paths, svn server urls and revisions)
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000683 that it can be used either to generate external svn/git commands (without
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000684 gclient) or as input to gclient's --rev option (with some massaging of
685 the data).
686
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000687 Since we care about the revision of the current source tree, for git
688 repositories this command uses the revision of the HEAD. For subversion we
689 use BASE.
690
691 The --snapshot option allows creating a .gclient file to reproduce the tree.
692
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000693 Raises:
694 Error: If the client has conflicting entries.
695 """
696 # Check for revision overrides.
697 revision_overrides = {}
698 for revision in self._options.revisions:
699 if revision.find("@") < 0:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000700 raise gclient_utils.Error(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000701 "Specify the full dependency when specifying a revision number.")
702 revision_elem = revision.split("@")
703 # Disallow conflicting revs
704 if revision_overrides.has_key(revision_elem[0]) and \
705 revision_overrides[revision_elem[0]] != revision_elem[1]:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000706 raise gclient_utils.Error(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000707 "Conflicting revision numbers specified.")
708 revision_overrides[revision_elem[0]] = revision_elem[1]
709
710 solutions = self.GetVar("solutions")
711 if not solutions:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000712 raise gclient_utils.Error("No solution specified")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000713
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000714 # Inner helper to generate base url and rev tuple
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000715 def GetURLAndRev(name, original_url):
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000716 url, _ = gclient_utils.SplitUrlRevision(original_url)
717 scm = gclient_scm.CreateSCM(original_url, self._root_dir, name)
718 return (url, scm.revinfo(self._options, [], None))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000719
maruel@chromium.orge3da35f2010-03-09 21:40:45 +0000720 # text of the snapshot gclient file
721 new_gclient = ""
722 # Dictionary of { path : SCM url } to ensure no duplicate solutions
723 solution_names = {}
nasser@codeaurora.orgde8f3522010-03-11 23:47:44 +0000724 entries = {}
725 entries_deps_content = {}
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000726 # Run on the base solutions first.
727 for solution in solutions:
maruel@chromium.orge3da35f2010-03-09 21:40:45 +0000728 # Dictionary of { path : SCM url } to describe the gclient checkout
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000729 name = solution["name"]
maruel@chromium.orge3da35f2010-03-09 21:40:45 +0000730 if name in solution_names:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000731 raise gclient_utils.Error("solution %s specified more than once" % name)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000732 (url, rev) = GetURLAndRev(name, solution["url"])
msb@chromium.org770ff9e2009-09-23 17:18:18 +0000733 entries[name] = "%s@%s" % (url, rev)
maruel@chromium.orge3da35f2010-03-09 21:40:45 +0000734 solution_names[name] = "%s@%s" % (url, rev)
maruel@chromium.org491c04b2010-05-17 18:17:44 +0000735 deps_file = solution.get("deps_file", self.DEPS_FILE)
nasser@codeaurora.org952d7c72010-03-01 20:41:01 +0000736 if '/' in deps_file or '\\' in deps_file:
737 raise gclient_utils.Error('deps_file name must not be a path, just a '
738 'filename.')
739 try:
740 deps_content = gclient_utils.FileRead(
741 os.path.join(self._root_dir, name, deps_file))
742 except IOError, e:
743 if e.errno != errno.ENOENT:
744 raise
745 deps_content = ""
746 entries_deps_content[name] = deps_content
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000747
nasser@codeaurora.orgde8f3522010-03-11 23:47:44 +0000748 # Process the dependencies next (sort alphanumerically to ensure that
749 # containing directories get populated first and for readability)
750 deps = self._ParseAllDeps(entries, entries_deps_content)
751 deps_to_process = deps.keys()
752 deps_to_process.sort()
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000753
nasser@codeaurora.orgde8f3522010-03-11 23:47:44 +0000754 # First pass for direct dependencies.
755 for d in deps_to_process:
756 if type(deps[d]) == str:
757 (url, rev) = GetURLAndRev(d, deps[d])
758 entries[d] = "%s@%s" % (url, rev)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000759
nasser@codeaurora.orgde8f3522010-03-11 23:47:44 +0000760 # Second pass for inherited deps (via the From keyword)
761 for d in deps_to_process:
tony@chromium.org4b5b1772010-04-08 01:52:56 +0000762 if isinstance(deps[d], self.FromImpl):
nasser@codeaurora.orgde8f3522010-03-11 23:47:44 +0000763 deps_parent_url = entries[deps[d].module_name]
764 if deps_parent_url.find("@") < 0:
765 raise gclient_utils.Error("From %s missing revisioned url" %
766 deps[d].module_name)
767 content = gclient_utils.FileRead(os.path.join(
768 self._root_dir,
769 deps[d].module_name,
maruel@chromium.org491c04b2010-05-17 18:17:44 +0000770 self.DEPS_FILE))
771 sub_deps = self._ParseSolutionDeps(deps[d].module_name, content, {},
772 False)
nasser@codeaurora.orgde8f3522010-03-11 23:47:44 +0000773 (url, rev) = GetURLAndRev(d, sub_deps[d])
774 entries[d] = "%s@%s" % (url, rev)
maruel@chromium.orge3da35f2010-03-09 21:40:45 +0000775
nasser@codeaurora.orgde8f3522010-03-11 23:47:44 +0000776 # Build the snapshot configuration string
777 if self._options.snapshot:
778 url = entries.pop(name)
779 custom_deps = ",\n ".join(["\"%s\": \"%s\"" % (x, entries[x])
780 for x in sorted(entries.keys())])
maruel@chromium.orge3da35f2010-03-09 21:40:45 +0000781
maruel@chromium.org1f7d1182010-05-17 18:17:38 +0000782 new_gclient += self.DEFAULT_SNAPSHOT_SOLUTION_TEXT % {
nasser@codeaurora.orgde8f3522010-03-11 23:47:44 +0000783 'solution_name': name,
784 'solution_url': url,
785 'safesync_url' : "",
786 'solution_deps': custom_deps,
787 }
788 else:
789 print(";\n".join(["%s: %s" % (x, entries[x])
790 for x in sorted(entries.keys())]))
maruel@chromium.orge3da35f2010-03-09 21:40:45 +0000791
792 # Print the snapshot configuration file
793 if self._options.snapshot:
maruel@chromium.org491c04b2010-05-17 18:17:44 +0000794 config = self.DEFAULT_SNAPSHOT_FILE_TEXT % {'solution_list': new_gclient}
maruel@chromium.orge3da35f2010-03-09 21:40:45 +0000795 snapclient = GClient(self._root_dir, self._options)
796 snapclient.SetConfig(config)
797 print(snapclient._config_content)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000798
799
maruel@chromium.org096469f2010-05-26 13:34:30 +0000800#### gclient commands.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000801
802
maruel@chromium.org096469f2010-05-26 13:34:30 +0000803def CMDcleanup(parser, args):
804 """Cleans up all working copies.
maruel@chromium.orgddff62d2010-05-17 21:02:36 +0000805
maruel@chromium.org096469f2010-05-26 13:34:30 +0000806Mostly svn-specific. Simply runs 'svn cleanup' for each module.
maruel@chromium.org79692d62010-05-14 18:57:13 +0000807"""
maruel@chromium.org096469f2010-05-26 13:34:30 +0000808 (options, args) = parser.parse_args(args)
maruel@chromium.org2806acc2009-05-15 12:33:34 +0000809 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000810 if not client:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000811 raise gclient_utils.Error("client not configured; see 'gclient config'")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000812 if options.verbose:
813 # Print out the .gclient file. This is longer than if we just printed the
814 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000815 print(client.ConfigContent())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000816 return client.RunOnDeps('cleanup', args)
817
818
maruel@chromium.org096469f2010-05-26 13:34:30 +0000819@attr('usage', '[url] [safesync url]')
820def CMDconfig(parser, args):
maruel@chromium.orgddff62d2010-05-17 21:02:36 +0000821 """Create a .gclient file in the current directory.
822
maruel@chromium.org096469f2010-05-26 13:34:30 +0000823This specifies the configuration for further commands. After update/sync,
maruel@chromium.org79692d62010-05-14 18:57:13 +0000824top-level DEPS files in each module are read to determine dependent
maruel@chromium.org096469f2010-05-26 13:34:30 +0000825modules to operate on as well. If optional [url] parameter is
maruel@chromium.org79692d62010-05-14 18:57:13 +0000826provided, then configuration is read from a specified Subversion server
maruel@chromium.org096469f2010-05-26 13:34:30 +0000827URL.
maruel@chromium.org79692d62010-05-14 18:57:13 +0000828"""
maruel@chromium.org096469f2010-05-26 13:34:30 +0000829 parser.add_option("--spec",
830 help="create a gclient file containing the provided "
831 "string. Due to Cygwin/Python brokenness, it "
832 "probably can't contain any newlines.")
833 parser.add_option("--name",
834 help="overrides the default name for the solution")
835 (options, args) = parser.parse_args(args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000836 if len(args) < 1 and not options.spec:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000837 raise gclient_utils.Error("required argument missing; see 'gclient help "
838 "config'")
maruel@chromium.org0329e672009-05-13 18:41:04 +0000839 if os.path.exists(options.config_filename):
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000840 raise gclient_utils.Error("%s file already exists in the current directory"
841 % options.config_filename)
maruel@chromium.org2806acc2009-05-15 12:33:34 +0000842 client = GClient('.', options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000843 if options.spec:
844 client.SetConfig(options.spec)
845 else:
maruel@chromium.org1ab7ffc2009-06-03 17:21:37 +0000846 base_url = args[0].rstrip('/')
iposva@chromium.org8cf7a392010-04-07 17:20:26 +0000847 if not options.name:
848 name = base_url.split("/")[-1]
849 else:
850 # specify an alternate relpath for the given URL.
851 name = options.name
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000852 safesync_url = ""
853 if len(args) > 1:
854 safesync_url = args[1]
855 client.SetDefaultConfig(name, base_url, safesync_url)
856 client.SaveConfig()
maruel@chromium.org79692d62010-05-14 18:57:13 +0000857 return 0
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000858
859
maruel@chromium.org096469f2010-05-26 13:34:30 +0000860def CMDexport(parser, args):
maruel@chromium.orgddff62d2010-05-17 21:02:36 +0000861 """Wrapper for svn export for all managed directories."""
maruel@chromium.org096469f2010-05-26 13:34:30 +0000862 (options, args) = parser.parse_args(args)
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +0000863 if len(args) != 1:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000864 raise gclient_utils.Error("Need directory name")
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +0000865 client = GClient.LoadCurrentConfig(options)
866
867 if not client:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000868 raise gclient_utils.Error("client not configured; see 'gclient config'")
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +0000869
870 if options.verbose:
871 # Print out the .gclient file. This is longer than if we just printed the
872 # client dict, but more legible, and it might contain helpful comments.
873 print(client.ConfigContent())
874 return client.RunOnDeps('export', args)
875
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000876
maruel@chromium.org096469f2010-05-26 13:34:30 +0000877@attr('epilog', """Example:
878 gclient pack > patch.txt
879 generate simple patch for configured client and dependences
880""")
881def CMDpack(parser, args):
maruel@chromium.org79692d62010-05-14 18:57:13 +0000882 """Generate a patch which can be applied at the root of the tree.
maruel@chromium.orgddff62d2010-05-17 21:02:36 +0000883
maruel@chromium.org096469f2010-05-26 13:34:30 +0000884Internally, runs 'svn diff'/'git diff' on each checked out module and
maruel@chromium.org79692d62010-05-14 18:57:13 +0000885dependencies, and performs minimal postprocessing of the output. The
886resulting patch is printed to stdout and can be applied to a freshly
maruel@chromium.org096469f2010-05-26 13:34:30 +0000887checked out tree via 'patch -p0 < patchfile'.
maruel@chromium.org79692d62010-05-14 18:57:13 +0000888"""
maruel@chromium.org096469f2010-05-26 13:34:30 +0000889 (options, args) = parser.parse_args(args)
kbr@google.comab318592009-09-04 00:54:55 +0000890 client = GClient.LoadCurrentConfig(options)
891 if not client:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000892 raise gclient_utils.Error("client not configured; see 'gclient config'")
kbr@google.comab318592009-09-04 00:54:55 +0000893 if options.verbose:
894 # Print out the .gclient file. This is longer than if we just printed the
895 # client dict, but more legible, and it might contain helpful comments.
896 print(client.ConfigContent())
kbr@google.comab318592009-09-04 00:54:55 +0000897 return client.RunOnDeps('pack', args)
898
899
maruel@chromium.org096469f2010-05-26 13:34:30 +0000900def CMDstatus(parser, args):
901 """Show modification status for every dependencies."""
902 (options, args) = parser.parse_args(args)
maruel@chromium.org2806acc2009-05-15 12:33:34 +0000903 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000904 if not client:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000905 raise gclient_utils.Error("client not configured; see 'gclient config'")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000906 if options.verbose:
907 # Print out the .gclient file. This is longer than if we just printed the
908 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000909 print(client.ConfigContent())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000910 return client.RunOnDeps('status', args)
911
912
maruel@chromium.org096469f2010-05-26 13:34:30 +0000913@attr('epilog', """Examples:
maruel@chromium.org79692d62010-05-14 18:57:13 +0000914 gclient sync
915 update files from SCM according to current configuration,
916 *for modules which have changed since last update or sync*
917 gclient sync --force
918 update files from SCM according to current configuration, for
919 all modules (useful for recovering files deleted from local copy)
920 gclient sync --revision src@31000
921 update src directory to r31000
maruel@chromium.org096469f2010-05-26 13:34:30 +0000922""")
923def CMDsync(parser, args):
924 """Checkout/update all modules."""
925 parser.add_option("--force", action="store_true",
926 help="force update even for unchanged modules")
927 parser.add_option("--nohooks", action="store_true",
928 help="don't run hooks after the update is complete")
929 parser.add_option("-r", "--revision", action="append",
930 dest="revisions", metavar="REV", default=[],
931 help="update given solution to specified revision, "
932 "can be used multiple times for each solution, "
933 "e.g. -r src@123, -r internal@32")
934 parser.add_option("--head", action="store_true",
935 help="skips any safesync_urls specified in "
936 "configured solutions and sync to head instead")
937 parser.add_option("--delete_unversioned_trees", action="store_true",
938 help="delete any unexpected unversioned trees "
939 "that are in the checkout")
940 parser.add_option("--reset", action="store_true",
941 help="resets any local changes before updating (git only)")
942 parser.add_option("--deps", dest="deps_os", metavar="OS_LIST",
943 help="sync deps for the specified (comma-separated) "
944 "platform(s); 'all' will sync all platforms")
945 parser.add_option("--manually_grab_svn_rev", action="store_true",
946 help="Skip svn up whenever possible by requesting "
947 "actual HEAD revision from the repository")
948 (options, args) = parser.parse_args(args)
maruel@chromium.org2806acc2009-05-15 12:33:34 +0000949 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000950
951 if not client:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000952 raise gclient_utils.Error("client not configured; see 'gclient config'")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000953
954 if not options.head:
955 solutions = client.GetVar('solutions')
956 if solutions:
957 for s in solutions:
958 if s.get('safesync_url', ''):
959 # rip through revisions and make sure we're not over-riding
960 # something that was explicitly passed
961 has_key = False
962 for r in options.revisions:
963 if r.split('@')[0] == s['name']:
964 has_key = True
965 break
966
967 if not has_key:
968 handle = urllib.urlopen(s['safesync_url'])
969 rev = handle.read().strip()
970 handle.close()
971 if len(rev):
972 options.revisions.append(s['name']+'@'+rev)
973
974 if options.verbose:
975 # Print out the .gclient file. This is longer than if we just printed the
976 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000977 print(client.ConfigContent())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000978 return client.RunOnDeps('update', args)
979
980
maruel@chromium.org096469f2010-05-26 13:34:30 +0000981def CMDupdate(parser, args):
maruel@chromium.orgddff62d2010-05-17 21:02:36 +0000982 """Alias for the sync command. Deprecated."""
maruel@chromium.org096469f2010-05-26 13:34:30 +0000983 return CMDsync(parser, args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000984
maruel@chromium.org096469f2010-05-26 13:34:30 +0000985def CMDdiff(parser, args):
986 """Displays local diff for every dependencies."""
987 (options, args) = parser.parse_args(args)
maruel@chromium.org2806acc2009-05-15 12:33:34 +0000988 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000989 if not client:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000990 raise gclient_utils.Error("client not configured; see 'gclient config'")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000991 if options.verbose:
992 # Print out the .gclient file. This is longer than if we just printed the
993 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000994 print(client.ConfigContent())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000995 return client.RunOnDeps('diff', args)
996
997
maruel@chromium.org096469f2010-05-26 13:34:30 +0000998def CMDrevert(parser, args):
999 """Revert all modifications in every dependencies."""
1000 parser.add_option("--nohooks", action="store_true",
1001 help="don't run hooks after the revert is complete")
1002 (options, args) = parser.parse_args(args)
1003 # --force is implied.
1004 options.force = True
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001005 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001006 if not client:
maruel@chromium.orge3608df2009-11-10 20:22:57 +00001007 raise gclient_utils.Error("client not configured; see 'gclient config'")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001008 return client.RunOnDeps('revert', args)
1009
1010
maruel@chromium.org096469f2010-05-26 13:34:30 +00001011def CMDrunhooks(parser, args):
1012 """Runs hooks for files that have been modified in the local working copy."""
1013 parser.add_option("--force", action="store_true", default=True,
1014 help="Deprecated. No effect.")
1015 (options, args) = parser.parse_args(args)
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001016 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001017 if not client:
maruel@chromium.orge3608df2009-11-10 20:22:57 +00001018 raise gclient_utils.Error("client not configured; see 'gclient config'")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001019 if options.verbose:
1020 # Print out the .gclient file. This is longer than if we just printed the
1021 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +00001022 print(client.ConfigContent())
maruel@chromium.org5df6a462009-08-28 18:52:26 +00001023 options.force = True
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001024 return client.RunOnDeps('runhooks', args)
1025
1026
maruel@chromium.org096469f2010-05-26 13:34:30 +00001027def CMDrevinfo(parser, args):
1028 """Outputs details for every dependencies."""
1029 parser.add_option("--snapshot", action="store_true",
1030 help="create a snapshot file of the current "
1031 "version of all repositories")
1032 (options, args) = parser.parse_args(args)
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001033 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001034 if not client:
maruel@chromium.orge3608df2009-11-10 20:22:57 +00001035 raise gclient_utils.Error("client not configured; see 'gclient config'")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001036 client.PrintRevInfo()
maruel@chromium.org79692d62010-05-14 18:57:13 +00001037 return 0
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001038
1039
maruel@chromium.org096469f2010-05-26 13:34:30 +00001040def Command(name):
1041 return getattr(sys.modules[__name__], 'CMD' + name, None)
1042
1043
1044def CMDhelp(parser, args):
1045 """Prints list of commands or help for a specific command."""
1046 (options, args) = parser.parse_args(args)
maruel@chromium.orgddff62d2010-05-17 21:02:36 +00001047 if len(args) == 1:
maruel@chromium.org096469f2010-05-26 13:34:30 +00001048 return Main(args + ['--help'])
maruel@chromium.orgddff62d2010-05-17 21:02:36 +00001049 parser.print_help()
1050 return 0
1051
1052
maruel@chromium.org096469f2010-05-26 13:34:30 +00001053def GenUsage(parser, command):
1054 """Modify an OptParse object with the function's documentation."""
1055 obj = Command(command)
1056 if command == 'help':
1057 command = '<command>'
1058 # OptParser.description prefer nicely non-formatted strings.
1059 parser.description = re.sub('[\r\n ]{2,}', ' ', obj.__doc__)
1060 usage = getattr(obj, 'usage', '')
1061 parser.set_usage('%%prog %s [options] %s' % (command, usage))
1062 parser.epilog = getattr(obj, 'epilog', None)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001063
1064
1065def Main(argv):
maruel@chromium.org096469f2010-05-26 13:34:30 +00001066 """Doesn't parse the arguments here, just find the right subcommand to
1067 execute."""
1068 # Do it late so all commands are listed.
1069 CMDhelp.usage = ('\n\nCommands are:\n' + '\n'.join([
1070 ' %-10s %s' % (fn[3:], Command(fn[3:]).__doc__.split('\n')[0].strip())
1071 for fn in dir(sys.modules[__name__]) if fn.startswith('CMD')]))
1072 parser = optparse.OptionParser(version='%prog ' + __version__)
maruel@chromium.orgddff62d2010-05-17 21:02:36 +00001073 parser.add_option("-v", "--verbose", action="count", default=0,
1074 help="Produces additional output for diagnostics. Can be "
1075 "used up to three times for more logging info.")
1076 parser.add_option("--gclientfile", metavar="FILENAME", dest="config_filename",
1077 default=os.environ.get("GCLIENT_FILE", ".gclient"),
1078 help="Specify an alternate .gclient file")
maruel@chromium.orgddff62d2010-05-17 21:02:36 +00001079 # Integrate standard options processing.
1080 old_parser = parser.parse_args
1081 def Parse(args):
1082 (options, args) = old_parser(args)
1083 if options.verbose == 2:
1084 logging.basicConfig(level=logging.INFO)
1085 elif options.verbose > 2:
1086 logging.basicConfig(level=logging.DEBUG)
1087 options.entries_filename = options.config_filename + "_entries"
maruel@chromium.org096469f2010-05-26 13:34:30 +00001088 if not hasattr(options, 'revisions'):
1089 # GClient.RunOnDeps expects it even if not applicable.
1090 options.revisions = []
maruel@chromium.orgddff62d2010-05-17 21:02:36 +00001091 return (options, args)
1092 parser.parse_args = Parse
1093 # We don't want wordwrapping in epilog (usually examples)
1094 parser.format_epilog = lambda _: parser.epilog or ''
maruel@chromium.org096469f2010-05-26 13:34:30 +00001095 if argv:
1096 command = Command(argv[0])
1097 if command:
1098 # "fix" the usage and the description now that we know the subcommand.
1099 GenUsage(parser, argv[0])
1100 return command(parser, argv[1:])
1101 # Not a known command. Default to help.
1102 GenUsage(parser, 'help')
1103 return CMDhelp(parser, argv)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001104
1105
1106if "__main__" == __name__:
1107 try:
maruel@chromium.orgddff62d2010-05-17 21:02:36 +00001108 sys.exit(Main(sys.argv[1:]))
maruel@chromium.orge3608df2009-11-10 20:22:57 +00001109 except gclient_utils.Error, e:
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +00001110 print >> sys.stderr, "Error: %s" % str(e)
maruel@chromium.orgddff62d2010-05-17 21:02:36 +00001111 sys.exit(1)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001112
1113# vim: ts=2:sw=2:tw=80:et: