blob: 5839f00c3ae9e0c9830b4d33a4e9814a2e33f35e [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.org5ca27692010-05-26 19:32:41 +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 if self._options.deps_os is not None:
342 deps_to_include = self._options.deps_os.split(",")
343 if "all" in deps_to_include:
maruel@chromium.org491c04b2010-05-17 18:17:44 +0000344 deps_to_include = list(set(self.deps_os_choices.itervalues()))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000345 else:
maruel@chromium.org491c04b2010-05-17 18:17:44 +0000346 deps_to_include = [self.deps_os_choices.get(sys.platform, "unix")]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000347
348 deps_to_include = set(deps_to_include)
349 for deps_os_key in deps_to_include:
350 os_deps = local_scope["deps_os"].get(deps_os_key, {})
351 if len(deps_to_include) > 1:
352 # Ignore any overrides when including deps for more than one
353 # platform, so we collect the broadest set of dependencies available.
354 # We may end up with the wrong revision of something for our
355 # platform, but this is the best we can do.
356 deps.update([x for x in os_deps.items() if not x[0] in deps])
357 else:
358 deps.update(os_deps)
359
tony@chromium.org30ef9ae2010-04-09 02:18:05 +0000360 if 'hooks' in local_scope and parse_hooks:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000361 self._deps_hooks.extend(local_scope['hooks'])
362
363 # If use_relative_paths is set in the DEPS file, regenerate
364 # the dictionary using paths relative to the directory containing
365 # the DEPS file.
366 if local_scope.get('use_relative_paths'):
367 rel_deps = {}
368 for d, url in deps.items():
369 # normpath is required to allow DEPS to use .. in their
370 # dependency local path.
371 rel_deps[os.path.normpath(os.path.join(solution_name, d))] = url
372 return rel_deps
373 else:
374 return deps
375
376 def _ParseAllDeps(self, solution_urls, solution_deps_content):
377 """Parse the complete list of dependencies for the client.
378
379 Args:
380 solution_urls: A dict mapping module names (as relative paths) to URLs
381 corresponding to the solutions specified by the client. This parameter
382 is passed as an optimization.
383 solution_deps_content: A dict mapping module names to the content
384 of their DEPS files
385
386 Returns:
387 A dict mapping module names (as relative paths) to URLs corresponding
388 to the entire set of dependencies to checkout for the given client.
389
390 Raises:
391 Error: If a dependency conflicts with another dependency or of a solution.
392 """
393 deps = {}
394 for solution in self.GetVar("solutions"):
395 custom_vars = solution.get("custom_vars", {})
396 solution_deps = self._ParseSolutionDeps(
397 solution["name"],
398 solution_deps_content[solution["name"]],
tony@chromium.org30ef9ae2010-04-09 02:18:05 +0000399 custom_vars,
400 True)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000401
402 # If a line is in custom_deps, but not in the solution, we want to append
403 # this line to the solution.
404 if "custom_deps" in solution:
405 for d in solution["custom_deps"]:
406 if d not in solution_deps:
407 solution_deps[d] = solution["custom_deps"][d]
408
409 for d in solution_deps:
410 if "custom_deps" in solution and d in solution["custom_deps"]:
411 # Dependency is overriden.
412 url = solution["custom_deps"][d]
413 if url is None:
414 continue
415 else:
416 url = solution_deps[d]
417 # if we have a From reference dependent on another solution, then
418 # just skip the From reference. When we pull deps for the solution,
419 # we will take care of this dependency.
420 #
421 # If multiple solutions all have the same From reference, then we
422 # should only add one to our list of dependencies.
tony@chromium.org4b5b1772010-04-08 01:52:56 +0000423 if isinstance(url, self.FromImpl):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000424 if url.module_name in solution_urls:
425 # Already parsed.
426 continue
427 if d in deps and type(deps[d]) != str:
428 if url.module_name == deps[d].module_name:
429 continue
tony@chromium.org4b5b1772010-04-08 01:52:56 +0000430 elif isinstance(url, str):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000431 parsed_url = urlparse.urlparse(url)
432 scheme = parsed_url[0]
433 if not scheme:
434 # A relative url. Fetch the real base.
435 path = parsed_url[2]
436 if path[0] != "/":
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000437 raise gclient_utils.Error(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000438 "relative DEPS entry \"%s\" must begin with a slash" % d)
msb@chromium.orge6f78352010-01-13 17:05:33 +0000439 # Create a scm just to query the full url.
440 scm = gclient_scm.CreateSCM(solution["url"], self._root_dir,
441 None)
442 url = scm.FullUrlForRelativeUrl(url)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000443 if d in deps and deps[d] != url:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000444 raise gclient_utils.Error(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000445 "Solutions have conflicting versions of dependency \"%s\"" % d)
446 if d in solution_urls and solution_urls[d] != url:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000447 raise gclient_utils.Error(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000448 "Dependency \"%s\" conflicts with specified solution" % d)
449 # Grab the dependency.
450 deps[d] = url
451 return deps
452
phajdan.jr@chromium.org71b40682009-07-31 23:40:09 +0000453 def _RunHookAction(self, hook_dict, matching_file_list):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000454 """Runs the action from a single hook.
455 """
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000456 logging.info(hook_dict)
457 logging.info(matching_file_list)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000458 command = hook_dict['action'][:]
459 if command[0] == 'python':
460 # If the hook specified "python" as the first item, the action is a
461 # Python script. Run it by starting a new copy of the same
462 # interpreter.
463 command[0] = sys.executable
464
phajdan.jr@chromium.org71b40682009-07-31 23:40:09 +0000465 if '$matching_files' in command:
phajdan.jr@chromium.org68f2e092009-08-06 17:05:35 +0000466 splice_index = command.index('$matching_files')
467 command[splice_index:splice_index + 1] = matching_file_list
phajdan.jr@chromium.org71b40682009-07-31 23:40:09 +0000468
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000469 # Use a discrete exit status code of 2 to indicate that a hook action
470 # failed. Users of this script may wish to treat hook action failures
471 # differently from VC failures.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000472 gclient_utils.SubprocessCall(command, self._root_dir, fail_status=2)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000473
474 def _RunHooks(self, command, file_list, is_using_git):
475 """Evaluates all hooks, running actions as needed.
476 """
477 # Hooks only run for these command types.
478 if not command in ('update', 'revert', 'runhooks'):
479 return
480
evan@chromium.org67820ef2009-07-27 17:23:00 +0000481 # Hooks only run when --nohooks is not specified
482 if self._options.nohooks:
483 return
484
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000485 # Get any hooks from the .gclient file.
486 hooks = self.GetVar("hooks", [])
487 # Add any hooks found in DEPS files.
488 hooks.extend(self._deps_hooks)
489
490 # If "--force" was specified, run all hooks regardless of what files have
491 # changed. If the user is using git, then we don't know what files have
492 # changed so we always run all hooks.
493 if self._options.force or is_using_git:
494 for hook_dict in hooks:
phajdan.jr@chromium.org71b40682009-07-31 23:40:09 +0000495 self._RunHookAction(hook_dict, [])
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000496 return
497
498 # Run hooks on the basis of whether the files from the gclient operation
499 # match each hook's pattern.
500 for hook_dict in hooks:
501 pattern = re.compile(hook_dict['pattern'])
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000502 matching_file_list = [f for f in file_list if pattern.search(f)]
phajdan.jr@chromium.org71b40682009-07-31 23:40:09 +0000503 if matching_file_list:
504 self._RunHookAction(hook_dict, matching_file_list)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000505
maruel@chromium.org918a9ae2010-05-28 15:50:30 +0000506 def _EnforceRevisions(self, solutions):
507 """Checks for revision overrides."""
508 revision_overrides = {}
maruel@chromium.org307d1792010-05-31 20:03:13 +0000509 if self._options.head:
510 return revision_overrides
511 for s in solutions:
512 if not s.get('safesync_url', None):
513 continue
514 handle = urllib.urlopen(s['safesync_url'])
515 rev = handle.read().strip()
516 handle.close()
517 if len(rev):
518 self._options.revisions.append('%s@%s' % (s['name'], rev))
519 if not self._options.revisions:
520 return revision_overrides
521 # --revision will take over safesync_url.
522 solutions_names = [s['name'] for s in solutions]
523 index = 0
524 for revision in self._options.revisions:
525 if not '@' in revision:
526 # Support for --revision 123
527 revision = '%s@%s' % (solutions_names[index], revision)
528 sol, rev = revision.split("@", 1)
529 if not sol in solutions_names:
530 #raise gclient_utils.Error('%s is not a valid solution.' % sol)
531 print >> sys.stderr, ('Please fix your script, having invalid '
532 '--revision flags will soon considered an error.')
533 else:
maruel@chromium.org918a9ae2010-05-28 15:50:30 +0000534 revision_overrides[sol] = rev
maruel@chromium.org307d1792010-05-31 20:03:13 +0000535 index += 1
maruel@chromium.org918a9ae2010-05-28 15:50:30 +0000536 return revision_overrides
537
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000538 def RunOnDeps(self, command, args):
539 """Runs a command on each dependency in a client and its dependencies.
540
541 The module's dependencies are specified in its top-level DEPS files.
542
543 Args:
544 command: The command to use (e.g., 'status' or 'diff')
545 args: list of str - extra arguments to add to the command line.
546
547 Raises:
548 Error: If the client has conflicting entries.
549 """
550 if not command in self.supported_commands:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000551 raise gclient_utils.Error("'%s' is an unsupported command" % command)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000552
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000553 solutions = self.GetVar("solutions")
554 if not solutions:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000555 raise gclient_utils.Error("No solution specified")
maruel@chromium.org918a9ae2010-05-28 15:50:30 +0000556 revision_overrides = self._EnforceRevisions(solutions)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000557
558 # When running runhooks --force, there's no need to consult the SCM.
559 # All known hooks are expected to run unconditionally regardless of working
560 # copy state, so skip the SCM status check.
561 run_scm = not (command == 'runhooks' and self._options.force)
562
563 entries = {}
564 entries_deps_content = {}
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000565 file_list = []
566 # Run on the base solutions first.
567 for solution in solutions:
568 name = solution["name"]
maruel@chromium.org1f7d1182010-05-17 18:17:38 +0000569 deps_file = solution.get("deps_file", self.DEPS_FILE)
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000570 if '/' in deps_file or '\\' in deps_file:
571 raise gclient_utils.Error('deps_file name must not be a path, just a '
572 'filename.')
573 if name in entries:
574 raise gclient_utils.Error("solution %s specified more than once" % name)
575 url = solution["url"]
576 entries[name] = url
577 if run_scm and url:
578 self._options.revision = revision_overrides.get(name)
579 scm = gclient_scm.CreateSCM(url, self._root_dir, name)
580 scm.RunCommand(command, self._options, args, file_list)
581 file_list = [os.path.join(name, f.strip()) for f in file_list]
582 self._options.revision = None
583 try:
584 deps_content = gclient_utils.FileRead(
585 os.path.join(self._root_dir, name, deps_file))
586 except IOError, e:
587 if e.errno != errno.ENOENT:
588 raise
589 deps_content = ""
590 entries_deps_content[name] = deps_content
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000591
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000592 # Process the dependencies next (sort alphanumerically to ensure that
593 # containing directories get populated first and for readability)
594 deps = self._ParseAllDeps(entries, entries_deps_content)
595 deps_to_process = deps.keys()
596 deps_to_process.sort()
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000597
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000598 # First pass for direct dependencies.
599 if command == 'update' and not self._options.verbose:
600 pm = Progress('Syncing projects', len(deps_to_process))
601 for d in deps_to_process:
nasser@codeaurora.org1f7a3d12010-02-04 15:11:50 +0000602 if command == 'update' and not self._options.verbose:
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000603 pm.update()
604 if type(deps[d]) == str:
605 url = deps[d]
606 entries[d] = url
607 if run_scm:
608 self._options.revision = revision_overrides.get(d)
609 scm = gclient_scm.CreateSCM(url, self._root_dir, d)
610 scm.RunCommand(command, self._options, args, file_list)
611 self._options.revision = None
612 elif isinstance(deps[d], self.FileImpl):
maruel@chromium.org491c04b2010-05-17 18:17:44 +0000613 file_dep = deps[d]
614 self._options.revision = file_dep.GetRevision()
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000615 if run_scm:
maruel@chromium.org491c04b2010-05-17 18:17:44 +0000616 scm = gclient_scm.CreateSCM(file_dep.GetPath(), self._root_dir, d)
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000617 scm.RunCommand("updatesingle", self._options,
maruel@chromium.org491c04b2010-05-17 18:17:44 +0000618 args + [file_dep.GetFilename()], file_list)
maruel@chromium.org79692d62010-05-14 18:57:13 +0000619
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000620 if command == 'update' and not self._options.verbose:
621 pm.end()
piman@chromium.org6f363722010-04-27 00:41:09 +0000622
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000623 # Second pass for inherited deps (via the From keyword)
624 for d in deps_to_process:
625 if isinstance(deps[d], self.FromImpl):
626 filename = os.path.join(self._root_dir,
627 deps[d].module_name,
maruel@chromium.org1f7d1182010-05-17 18:17:38 +0000628 self.DEPS_FILE)
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000629 content = gclient_utils.FileRead(filename)
630 sub_deps = self._ParseSolutionDeps(deps[d].module_name, content, {},
631 False)
632 # Getting the URL from the sub_deps file can involve having to resolve
633 # a File() or having to resolve a relative URL. To resolve relative
634 # URLs, we need to pass in the orignal sub deps URL.
635 sub_deps_base_url = deps[deps[d].module_name]
636 url = deps[d].GetUrl(d, sub_deps_base_url, self._root_dir, sub_deps)
637 entries[d] = url
638 if run_scm:
639 self._options.revision = revision_overrides.get(d)
640 scm = gclient_scm.CreateSCM(url, self._root_dir, d)
641 scm.RunCommand(command, self._options, args, file_list)
642 self._options.revision = None
gspencer@google.comdf2d5902009-09-11 22:16:21 +0000643
phajdan.jr@chromium.orgd83b2b22009-08-11 15:30:55 +0000644 # Convert all absolute paths to relative.
645 for i in range(len(file_list)):
646 # TODO(phajdan.jr): We should know exactly when the paths are absolute.
647 # It depends on the command being executed (like runhooks vs sync).
648 if not os.path.isabs(file_list[i]):
649 continue
650
651 prefix = os.path.commonprefix([self._root_dir.lower(),
652 file_list[i].lower()])
653 file_list[i] = file_list[i][len(prefix):]
654
655 # Strip any leading path separators.
656 while file_list[i].startswith('\\') or file_list[i].startswith('/'):
657 file_list[i] = file_list[i][1:]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000658
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000659 is_using_git = gclient_utils.IsUsingGit(self._root_dir, entries.keys())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000660 self._RunHooks(command, file_list, is_using_git)
661
662 if command == 'update':
ajwong@chromium.orgcdcee802009-06-23 15:30:42 +0000663 # Notify the user if there is an orphaned entry in their working copy.
664 # Only delete the directory if there are no changes in it, and
665 # delete_unversioned_trees is set to true.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000666 prev_entries = self._ReadEntries()
667 for entry in prev_entries:
maruel@chromium.orgc5e9aec2009-08-03 18:25:56 +0000668 # Fix path separator on Windows.
669 entry_fixed = entry.replace('/', os.path.sep)
670 e_dir = os.path.join(self._root_dir, entry_fixed)
671 # Use entry and not entry_fixed there.
maruel@chromium.org0329e672009-05-13 18:41:04 +0000672 if entry not in entries and os.path.exists(e_dir):
msb@chromium.org83017012009-09-28 18:52:12 +0000673 modified_files = False
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000674 if isinstance(prev_entries, list):
msb@chromium.org83017012009-09-28 18:52:12 +0000675 # old .gclient_entries format was list, now dict
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000676 modified_files = gclient_scm.scm.SVN.CaptureStatus(e_dir)
msb@chromium.org83017012009-09-28 18:52:12 +0000677 else:
678 file_list = []
679 scm = gclient_scm.CreateSCM(prev_entries[entry], self._root_dir,
680 entry_fixed)
681 scm.status(self._options, [], file_list)
682 modified_files = file_list != []
683 if not self._options.delete_unversioned_trees or modified_files:
maruel@chromium.orgc5e9aec2009-08-03 18:25:56 +0000684 # There are modified files in this entry. Keep warning until
685 # removed.
maruel@chromium.orgc5e9aec2009-08-03 18:25:56 +0000686 print(("\nWARNING: \"%s\" is no longer part of this client. "
687 "It is recommended that you manually remove it.\n") %
688 entry_fixed)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000689 else:
690 # Delete the entry
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000691 print("\n________ deleting \'%s\' " +
maruel@chromium.orgc5e9aec2009-08-03 18:25:56 +0000692 "in \'%s\'") % (entry_fixed, self._root_dir)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000693 gclient_utils.RemoveDirectory(e_dir)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000694 # record the current list of entries for next time
695 self._SaveEntries(entries)
maruel@chromium.org17cdf762010-05-28 17:30:52 +0000696 return 0
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000697
698 def PrintRevInfo(self):
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000699 """Output revision info mapping for the client and its dependencies.
700
701 This allows the capture of an overall "revision" for the source tree that
maruel@chromium.org918a9ae2010-05-28 15:50:30 +0000702 can be used to reproduce the same tree in the future. It is only useful for
703 "unpinned dependencies", i.e. DEPS/deps references without a svn revision
704 number or a git hash. A git branch name isn't "pinned" since the actual
705 commit can change.
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000706
707 The --snapshot option allows creating a .gclient file to reproduce the tree.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000708 """
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000709 solutions = self.GetVar("solutions")
710 if not solutions:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000711 raise gclient_utils.Error("No solution specified")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000712
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000713 # Inner helper to generate base url and rev tuple
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000714 def GetURLAndRev(name, original_url):
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000715 url, _ = gclient_utils.SplitUrlRevision(original_url)
716 scm = gclient_scm.CreateSCM(original_url, self._root_dir, name)
717 return (url, scm.revinfo(self._options, [], None))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000718
maruel@chromium.orge3da35f2010-03-09 21:40:45 +0000719 # text of the snapshot gclient file
720 new_gclient = ""
721 # Dictionary of { path : SCM url } to ensure no duplicate solutions
722 solution_names = {}
nasser@codeaurora.orgde8f3522010-03-11 23:47:44 +0000723 entries = {}
724 entries_deps_content = {}
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000725 # Run on the base solutions first.
726 for solution in solutions:
maruel@chromium.orge3da35f2010-03-09 21:40:45 +0000727 # Dictionary of { path : SCM url } to describe the gclient checkout
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000728 name = solution["name"]
maruel@chromium.orge3da35f2010-03-09 21:40:45 +0000729 if name in solution_names:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000730 raise gclient_utils.Error("solution %s specified more than once" % name)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000731 (url, rev) = GetURLAndRev(name, solution["url"])
msb@chromium.org770ff9e2009-09-23 17:18:18 +0000732 entries[name] = "%s@%s" % (url, rev)
maruel@chromium.orge3da35f2010-03-09 21:40:45 +0000733 solution_names[name] = "%s@%s" % (url, rev)
maruel@chromium.org491c04b2010-05-17 18:17:44 +0000734 deps_file = solution.get("deps_file", self.DEPS_FILE)
nasser@codeaurora.org952d7c72010-03-01 20:41:01 +0000735 if '/' in deps_file or '\\' in deps_file:
736 raise gclient_utils.Error('deps_file name must not be a path, just a '
737 'filename.')
738 try:
739 deps_content = gclient_utils.FileRead(
740 os.path.join(self._root_dir, name, deps_file))
741 except IOError, e:
742 if e.errno != errno.ENOENT:
743 raise
744 deps_content = ""
745 entries_deps_content[name] = deps_content
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000746
nasser@codeaurora.orgde8f3522010-03-11 23:47:44 +0000747 # Process the dependencies next (sort alphanumerically to ensure that
748 # containing directories get populated first and for readability)
749 deps = self._ParseAllDeps(entries, entries_deps_content)
750 deps_to_process = deps.keys()
751 deps_to_process.sort()
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000752
nasser@codeaurora.orgde8f3522010-03-11 23:47:44 +0000753 # First pass for direct dependencies.
754 for d in deps_to_process:
755 if type(deps[d]) == str:
756 (url, rev) = GetURLAndRev(d, deps[d])
757 entries[d] = "%s@%s" % (url, rev)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000758
nasser@codeaurora.orgde8f3522010-03-11 23:47:44 +0000759 # Second pass for inherited deps (via the From keyword)
760 for d in deps_to_process:
tony@chromium.org4b5b1772010-04-08 01:52:56 +0000761 if isinstance(deps[d], self.FromImpl):
nasser@codeaurora.orgde8f3522010-03-11 23:47:44 +0000762 deps_parent_url = entries[deps[d].module_name]
763 if deps_parent_url.find("@") < 0:
764 raise gclient_utils.Error("From %s missing revisioned url" %
765 deps[d].module_name)
766 content = gclient_utils.FileRead(os.path.join(
767 self._root_dir,
768 deps[d].module_name,
maruel@chromium.org491c04b2010-05-17 18:17:44 +0000769 self.DEPS_FILE))
770 sub_deps = self._ParseSolutionDeps(deps[d].module_name, content, {},
771 False)
nasser@codeaurora.orgde8f3522010-03-11 23:47:44 +0000772 (url, rev) = GetURLAndRev(d, sub_deps[d])
773 entries[d] = "%s@%s" % (url, rev)
maruel@chromium.orge3da35f2010-03-09 21:40:45 +0000774
nasser@codeaurora.orgde8f3522010-03-11 23:47:44 +0000775 # Build the snapshot configuration string
776 if self._options.snapshot:
777 url = entries.pop(name)
778 custom_deps = ",\n ".join(["\"%s\": \"%s\"" % (x, entries[x])
779 for x in sorted(entries.keys())])
maruel@chromium.orge3da35f2010-03-09 21:40:45 +0000780
maruel@chromium.org1f7d1182010-05-17 18:17:38 +0000781 new_gclient += self.DEFAULT_SNAPSHOT_SOLUTION_TEXT % {
nasser@codeaurora.orgde8f3522010-03-11 23:47:44 +0000782 'solution_name': name,
783 'solution_url': url,
784 'safesync_url' : "",
785 'solution_deps': custom_deps,
786 }
787 else:
788 print(";\n".join(["%s: %s" % (x, entries[x])
789 for x in sorted(entries.keys())]))
maruel@chromium.orge3da35f2010-03-09 21:40:45 +0000790
791 # Print the snapshot configuration file
792 if self._options.snapshot:
maruel@chromium.org491c04b2010-05-17 18:17:44 +0000793 config = self.DEFAULT_SNAPSHOT_FILE_TEXT % {'solution_list': new_gclient}
maruel@chromium.orge3da35f2010-03-09 21:40:45 +0000794 snapclient = GClient(self._root_dir, self._options)
795 snapclient.SetConfig(config)
796 print(snapclient._config_content)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000797
798
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000799#### gclient commands.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000800
801
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000802def CMDcleanup(parser, args):
803 """Cleans up all working copies.
maruel@chromium.orgddff62d2010-05-17 21:02:36 +0000804
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000805Mostly svn-specific. Simply runs 'svn cleanup' for each module.
maruel@chromium.org79692d62010-05-14 18:57:13 +0000806"""
maruel@chromium.org86c0dec2010-05-28 19:01:00 +0000807 parser.add_option("--deps", dest="deps_os", metavar="OS_LIST",
808 help="override deps for the specified (comma-separated) "
809 "platform(s); 'all' will process all deps_os "
810 "references")
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000811 (options, args) = parser.parse_args(args)
maruel@chromium.org2806acc2009-05-15 12:33:34 +0000812 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000813 if not client:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000814 raise gclient_utils.Error("client not configured; see 'gclient config'")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000815 if options.verbose:
816 # Print out the .gclient file. This is longer than if we just printed the
817 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000818 print(client.ConfigContent())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000819 return client.RunOnDeps('cleanup', args)
820
821
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000822@attr('usage', '[url] [safesync url]')
823def CMDconfig(parser, args):
maruel@chromium.orgddff62d2010-05-17 21:02:36 +0000824 """Create a .gclient file in the current directory.
825
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000826This specifies the configuration for further commands. After update/sync,
maruel@chromium.org79692d62010-05-14 18:57:13 +0000827top-level DEPS files in each module are read to determine dependent
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000828modules to operate on as well. If optional [url] parameter is
maruel@chromium.org79692d62010-05-14 18:57:13 +0000829provided, then configuration is read from a specified Subversion server
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000830URL.
maruel@chromium.org79692d62010-05-14 18:57:13 +0000831"""
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000832 parser.add_option("--spec",
833 help="create a gclient file containing the provided "
834 "string. Due to Cygwin/Python brokenness, it "
835 "probably can't contain any newlines.")
836 parser.add_option("--name",
837 help="overrides the default name for the solution")
838 (options, args) = parser.parse_args(args)
maruel@chromium.org5fc2a332010-05-26 19:37:15 +0000839 if ((options.spec and args) or len(args) > 2 or
840 (not options.spec and not args)):
841 parser.error('Inconsistent arguments. Use either --spec or one or 2 args')
842
maruel@chromium.org0329e672009-05-13 18:41:04 +0000843 if os.path.exists(options.config_filename):
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000844 raise gclient_utils.Error("%s file already exists in the current directory"
845 % options.config_filename)
maruel@chromium.org2806acc2009-05-15 12:33:34 +0000846 client = GClient('.', options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000847 if options.spec:
848 client.SetConfig(options.spec)
849 else:
maruel@chromium.org1ab7ffc2009-06-03 17:21:37 +0000850 base_url = args[0].rstrip('/')
iposva@chromium.org8cf7a392010-04-07 17:20:26 +0000851 if not options.name:
852 name = base_url.split("/")[-1]
853 else:
854 # specify an alternate relpath for the given URL.
855 name = options.name
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000856 safesync_url = ""
857 if len(args) > 1:
858 safesync_url = args[1]
859 client.SetDefaultConfig(name, base_url, safesync_url)
860 client.SaveConfig()
maruel@chromium.org79692d62010-05-14 18:57:13 +0000861 return 0
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000862
863
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000864def CMDexport(parser, args):
maruel@chromium.orgddff62d2010-05-17 21:02:36 +0000865 """Wrapper for svn export for all managed directories."""
maruel@chromium.org86c0dec2010-05-28 19:01:00 +0000866 parser.add_option("--deps", dest="deps_os", metavar="OS_LIST",
867 help="override deps for the specified (comma-separated) "
868 "platform(s); 'all' will process all deps_os "
869 "references")
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000870 (options, args) = parser.parse_args(args)
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +0000871 if len(args) != 1:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000872 raise gclient_utils.Error("Need directory name")
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +0000873 client = GClient.LoadCurrentConfig(options)
874
875 if not client:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000876 raise gclient_utils.Error("client not configured; see 'gclient config'")
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +0000877
878 if options.verbose:
879 # Print out the .gclient file. This is longer than if we just printed the
880 # client dict, but more legible, and it might contain helpful comments.
881 print(client.ConfigContent())
882 return client.RunOnDeps('export', args)
883
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000884
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000885@attr('epilog', """Example:
886 gclient pack > patch.txt
887 generate simple patch for configured client and dependences
888""")
889def CMDpack(parser, args):
maruel@chromium.org79692d62010-05-14 18:57:13 +0000890 """Generate a patch which can be applied at the root of the tree.
maruel@chromium.orgddff62d2010-05-17 21:02:36 +0000891
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000892Internally, runs 'svn diff'/'git diff' on each checked out module and
maruel@chromium.org79692d62010-05-14 18:57:13 +0000893dependencies, and performs minimal postprocessing of the output. The
894resulting patch is printed to stdout and can be applied to a freshly
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000895checked out tree via 'patch -p0 < patchfile'.
maruel@chromium.org79692d62010-05-14 18:57:13 +0000896"""
maruel@chromium.org86c0dec2010-05-28 19:01:00 +0000897 parser.add_option("--deps", dest="deps_os", metavar="OS_LIST",
898 help="override deps for the specified (comma-separated) "
899 "platform(s); 'all' will process all deps_os "
900 "references")
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000901 (options, args) = parser.parse_args(args)
kbr@google.comab318592009-09-04 00:54:55 +0000902 client = GClient.LoadCurrentConfig(options)
903 if not client:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000904 raise gclient_utils.Error("client not configured; see 'gclient config'")
kbr@google.comab318592009-09-04 00:54:55 +0000905 if options.verbose:
906 # Print out the .gclient file. This is longer than if we just printed the
907 # client dict, but more legible, and it might contain helpful comments.
908 print(client.ConfigContent())
kbr@google.comab318592009-09-04 00:54:55 +0000909 return client.RunOnDeps('pack', args)
910
911
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000912def CMDstatus(parser, args):
913 """Show modification status for every dependencies."""
maruel@chromium.org6b1d00b2010-05-26 20:11:08 +0000914 parser.add_option("--deps", dest="deps_os", metavar="OS_LIST",
maruel@chromium.org86c0dec2010-05-28 19:01:00 +0000915 help="override deps for the specified (comma-separated) "
916 "platform(s); 'all' will process all deps_os "
917 "references")
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000918 (options, args) = parser.parse_args(args)
maruel@chromium.org2806acc2009-05-15 12:33:34 +0000919 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000920 if not client:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000921 raise gclient_utils.Error("client not configured; see 'gclient config'")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000922 if options.verbose:
923 # Print out the .gclient file. This is longer than if we just printed the
924 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000925 print(client.ConfigContent())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000926 return client.RunOnDeps('status', args)
927
928
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000929@attr('epilog', """Examples:
maruel@chromium.org79692d62010-05-14 18:57:13 +0000930 gclient sync
931 update files from SCM according to current configuration,
932 *for modules which have changed since last update or sync*
933 gclient sync --force
934 update files from SCM according to current configuration, for
935 all modules (useful for recovering files deleted from local copy)
936 gclient sync --revision src@31000
937 update src directory to r31000
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000938""")
939def CMDsync(parser, args):
940 """Checkout/update all modules."""
941 parser.add_option("--force", action="store_true",
942 help="force update even for unchanged modules")
943 parser.add_option("--nohooks", action="store_true",
944 help="don't run hooks after the update is complete")
945 parser.add_option("-r", "--revision", action="append",
946 dest="revisions", metavar="REV", default=[],
maruel@chromium.org307d1792010-05-31 20:03:13 +0000947 help="Enforces revision/hash for the solutions with the "
948 "format src@rev. The src@ part is optional and can be "
949 "skipped. -r can be used multiple times when .gclient "
950 "has multiple solutions configured and will work even "
951 "if the src@ part is skipped.")
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000952 parser.add_option("--head", action="store_true",
953 help="skips any safesync_urls specified in "
954 "configured solutions and sync to head instead")
955 parser.add_option("--delete_unversioned_trees", action="store_true",
956 help="delete any unexpected unversioned trees "
957 "that are in the checkout")
958 parser.add_option("--reset", action="store_true",
959 help="resets any local changes before updating (git only)")
960 parser.add_option("--deps", dest="deps_os", metavar="OS_LIST",
maruel@chromium.org86c0dec2010-05-28 19:01:00 +0000961 help="override deps for the specified (comma-separated) "
962 "platform(s); 'all' will process all deps_os "
963 "references")
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000964 parser.add_option("--manually_grab_svn_rev", action="store_true",
965 help="Skip svn up whenever possible by requesting "
966 "actual HEAD revision from the repository")
967 (options, args) = parser.parse_args(args)
maruel@chromium.org2806acc2009-05-15 12:33:34 +0000968 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000969
970 if not client:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000971 raise gclient_utils.Error("client not configured; see 'gclient config'")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000972
maruel@chromium.org307d1792010-05-31 20:03:13 +0000973 if options.revisions and options.head:
974 # TODO(maruel): Make it a parser.error if it doesn't break any builder.
975 print("Warning: you cannot use both --head and --revision")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000976
977 if options.verbose:
978 # Print out the .gclient file. This is longer than if we just printed the
979 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000980 print(client.ConfigContent())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000981 return client.RunOnDeps('update', args)
982
983
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000984def CMDupdate(parser, args):
maruel@chromium.orgddff62d2010-05-17 21:02:36 +0000985 """Alias for the sync command. Deprecated."""
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000986 return CMDsync(parser, args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000987
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000988def CMDdiff(parser, args):
989 """Displays local diff for every dependencies."""
maruel@chromium.org86c0dec2010-05-28 19:01:00 +0000990 parser.add_option("--deps", dest="deps_os", metavar="OS_LIST",
991 help="override deps for the specified (comma-separated) "
992 "platform(s); 'all' will process all deps_os "
993 "references")
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000994 (options, args) = parser.parse_args(args)
maruel@chromium.org2806acc2009-05-15 12:33:34 +0000995 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000996 if not client:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000997 raise gclient_utils.Error("client not configured; see 'gclient config'")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000998 if options.verbose:
999 # Print out the .gclient file. This is longer than if we just printed the
1000 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +00001001 print(client.ConfigContent())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001002 return client.RunOnDeps('diff', args)
1003
1004
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001005def CMDrevert(parser, args):
1006 """Revert all modifications in every dependencies."""
maruel@chromium.org6b1d00b2010-05-26 20:11:08 +00001007 parser.add_option("--deps", dest="deps_os", metavar="OS_LIST",
maruel@chromium.org86c0dec2010-05-28 19:01:00 +00001008 help="override deps for the specified (comma-separated) "
1009 "platform(s); 'all' will process all deps_os "
1010 "references")
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001011 parser.add_option("--nohooks", action="store_true",
1012 help="don't run hooks after the revert is complete")
1013 (options, args) = parser.parse_args(args)
1014 # --force is implied.
1015 options.force = True
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 return client.RunOnDeps('revert', args)
1020
1021
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001022def CMDrunhooks(parser, args):
1023 """Runs hooks for files that have been modified in the local working copy."""
maruel@chromium.org6b1d00b2010-05-26 20:11:08 +00001024 parser.add_option("--deps", dest="deps_os", metavar="OS_LIST",
maruel@chromium.org86c0dec2010-05-28 19:01:00 +00001025 help="override deps for the specified (comma-separated) "
1026 "platform(s); 'all' will process all deps_os "
1027 "references")
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001028 parser.add_option("--force", action="store_true", default=True,
1029 help="Deprecated. No effect.")
1030 (options, args) = parser.parse_args(args)
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001031 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001032 if not client:
maruel@chromium.orge3608df2009-11-10 20:22:57 +00001033 raise gclient_utils.Error("client not configured; see 'gclient config'")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001034 if options.verbose:
1035 # Print out the .gclient file. This is longer than if we just printed the
1036 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +00001037 print(client.ConfigContent())
maruel@chromium.org5df6a462009-08-28 18:52:26 +00001038 options.force = True
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001039 options.nohooks = False
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001040 return client.RunOnDeps('runhooks', args)
1041
1042
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001043def CMDrevinfo(parser, args):
1044 """Outputs details for every dependencies."""
maruel@chromium.org86c0dec2010-05-28 19:01:00 +00001045 parser.add_option("--deps", dest="deps_os", metavar="OS_LIST",
1046 help="override deps for the specified (comma-separated) "
1047 "platform(s); 'all' will process all deps_os "
1048 "references")
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001049 parser.add_option("--snapshot", action="store_true",
1050 help="create a snapshot file of the current "
1051 "version of all repositories")
1052 (options, args) = parser.parse_args(args)
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001053 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001054 if not client:
maruel@chromium.orge3608df2009-11-10 20:22:57 +00001055 raise gclient_utils.Error("client not configured; see 'gclient config'")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001056 client.PrintRevInfo()
maruel@chromium.org79692d62010-05-14 18:57:13 +00001057 return 0
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001058
1059
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001060def Command(name):
1061 return getattr(sys.modules[__name__], 'CMD' + name, None)
1062
1063
1064def CMDhelp(parser, args):
1065 """Prints list of commands or help for a specific command."""
1066 (options, args) = parser.parse_args(args)
maruel@chromium.orgddff62d2010-05-17 21:02:36 +00001067 if len(args) == 1:
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001068 return Main(args + ['--help'])
maruel@chromium.orgddff62d2010-05-17 21:02:36 +00001069 parser.print_help()
1070 return 0
1071
1072
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001073def GenUsage(parser, command):
1074 """Modify an OptParse object with the function's documentation."""
1075 obj = Command(command)
1076 if command == 'help':
1077 command = '<command>'
1078 # OptParser.description prefer nicely non-formatted strings.
1079 parser.description = re.sub('[\r\n ]{2,}', ' ', obj.__doc__)
1080 usage = getattr(obj, 'usage', '')
1081 parser.set_usage('%%prog %s [options] %s' % (command, usage))
1082 parser.epilog = getattr(obj, 'epilog', None)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001083
1084
1085def Main(argv):
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001086 """Doesn't parse the arguments here, just find the right subcommand to
1087 execute."""
1088 # Do it late so all commands are listed.
1089 CMDhelp.usage = ('\n\nCommands are:\n' + '\n'.join([
1090 ' %-10s %s' % (fn[3:], Command(fn[3:]).__doc__.split('\n')[0].strip())
1091 for fn in dir(sys.modules[__name__]) if fn.startswith('CMD')]))
1092 parser = optparse.OptionParser(version='%prog ' + __version__)
maruel@chromium.orgddff62d2010-05-17 21:02:36 +00001093 parser.add_option("-v", "--verbose", action="count", default=0,
1094 help="Produces additional output for diagnostics. Can be "
1095 "used up to three times for more logging info.")
1096 parser.add_option("--gclientfile", metavar="FILENAME", dest="config_filename",
1097 default=os.environ.get("GCLIENT_FILE", ".gclient"),
1098 help="Specify an alternate .gclient file")
maruel@chromium.orgddff62d2010-05-17 21:02:36 +00001099 # Integrate standard options processing.
1100 old_parser = parser.parse_args
1101 def Parse(args):
1102 (options, args) = old_parser(args)
1103 if options.verbose == 2:
1104 logging.basicConfig(level=logging.INFO)
1105 elif options.verbose > 2:
1106 logging.basicConfig(level=logging.DEBUG)
1107 options.entries_filename = options.config_filename + "_entries"
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001108 if not hasattr(options, 'revisions'):
1109 # GClient.RunOnDeps expects it even if not applicable.
1110 options.revisions = []
maruel@chromium.org307d1792010-05-31 20:03:13 +00001111 if not hasattr(options, 'head'):
1112 options.head = None
maruel@chromium.orgddff62d2010-05-17 21:02:36 +00001113 return (options, args)
1114 parser.parse_args = Parse
1115 # We don't want wordwrapping in epilog (usually examples)
1116 parser.format_epilog = lambda _: parser.epilog or ''
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001117 if argv:
1118 command = Command(argv[0])
1119 if command:
1120 # "fix" the usage and the description now that we know the subcommand.
1121 GenUsage(parser, argv[0])
1122 return command(parser, argv[1:])
1123 # Not a known command. Default to help.
1124 GenUsage(parser, 'help')
1125 return CMDhelp(parser, argv)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001126
1127
1128if "__main__" == __name__:
1129 try:
maruel@chromium.orgddff62d2010-05-17 21:02:36 +00001130 sys.exit(Main(sys.argv[1:]))
maruel@chromium.orge3608df2009-11-10 20:22:57 +00001131 except gclient_utils.Error, e:
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +00001132 print >> sys.stderr, "Error: %s" % str(e)
maruel@chromium.orgddff62d2010-05-17 21:02:36 +00001133 sys.exit(1)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001134
1135# vim: ts=2:sw=2:tw=80:et: