blob: 9ea7ce08c5b27382836d94aaa5d0622434d23e8c [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
maruel@chromium.org116704f2010-06-11 17:34:38 +000088class GClientKeywords(object):
89 class FromImpl(object):
90 """Used to implement the From() syntax."""
91
92 def __init__(self, module_name, sub_target_name=None):
93 """module_name is the dep module we want to include from. It can also be
94 the name of a subdirectory to include from.
95
96 sub_target_name is an optional parameter if the module name in the other
97 DEPS file is different. E.g., you might want to map src/net to net."""
98 self.module_name = module_name
99 self.sub_target_name = sub_target_name
100
101 def __str__(self):
102 return 'From(%s, %s)' % (repr(self.module_name),
103 repr(self.sub_target_name))
104
105 def GetUrl(self, target_name, sub_deps_base_url, root_dir, sub_deps):
106 """Resolve the URL for this From entry."""
107 sub_deps_target_name = target_name
108 if self.sub_target_name:
109 sub_deps_target_name = self.sub_target_name
110 url = sub_deps[sub_deps_target_name]
111 if url.startswith('/'):
112 # If it's a relative URL, we need to resolve the URL relative to the
113 # sub deps base URL.
114 if not isinstance(sub_deps_base_url, basestring):
115 sub_deps_base_url = sub_deps_base_url.GetPath()
116 scm = gclient_scm.CreateSCM(sub_deps_base_url, root_dir,
117 None)
118 url = scm.FullUrlForRelativeUrl(url)
119 return url
120
121 class FileImpl(object):
122 """Used to implement the File('') syntax which lets you sync a single file
123 from an SVN repo."""
124
125 def __init__(self, file_location):
126 self.file_location = file_location
127
128 def __str__(self):
129 return 'File("%s")' % self.file_location
130
131 def GetPath(self):
132 return os.path.split(self.file_location)[0]
133
134 def GetFilename(self):
135 rev_tokens = self.file_location.split('@')
136 return os.path.split(rev_tokens[0])[1]
137
138 def GetRevision(self):
139 rev_tokens = self.file_location.split('@')
140 if len(rev_tokens) > 1:
141 return rev_tokens[1]
142 return None
143
144 class VarImpl(object):
145 def __init__(self, custom_vars, local_scope):
146 self._custom_vars = custom_vars
147 self._local_scope = local_scope
148
149 def Lookup(self, var_name):
150 """Implements the Var syntax."""
151 if var_name in self._custom_vars:
152 return self._custom_vars[var_name]
153 elif var_name in self._local_scope.get("vars", {}):
154 return self._local_scope["vars"][var_name]
155 raise gclient_utils.Error("Var is not defined: %s" % var_name)
156
157
158class GClient(GClientKeywords):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000159 """Object that represent a gclient checkout."""
maruel@chromium.org9eda4112010-06-11 18:56:10 +0000160 DEPS_FILE = 'DEPS'
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000161
maruel@chromium.org116704f2010-06-11 17:34:38 +0000162 SUPPORTED_COMMANDS = [
kbr@google.comab318592009-09-04 00:54:55 +0000163 'cleanup', 'diff', 'export', 'pack', 'revert', 'status', 'update',
164 'runhooks'
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000165 ]
166
maruel@chromium.org116704f2010-06-11 17:34:38 +0000167 DEPS_OS_CHOICES = {
maruel@chromium.org491c04b2010-05-17 18:17:44 +0000168 "win32": "win",
169 "win": "win",
170 "cygwin": "win",
171 "darwin": "mac",
172 "mac": "mac",
173 "unix": "unix",
174 "linux": "unix",
175 "linux2": "unix",
176 }
177
maruel@chromium.org1f7d1182010-05-17 18:17:38 +0000178 DEFAULT_CLIENT_FILE_TEXT = ("""\
179solutions = [
180 { "name" : "%(solution_name)s",
181 "url" : "%(solution_url)s",
182 "custom_deps" : {
183 },
184 "safesync_url": "%(safesync_url)s"
185 },
186]
187""")
188
189 DEFAULT_SNAPSHOT_SOLUTION_TEXT = ("""\
190 { "name" : "%(solution_name)s",
191 "url" : "%(solution_url)s",
192 "custom_deps" : {
193 %(solution_deps)s,
194 },
195 "safesync_url": "%(safesync_url)s"
196 },
197""")
198
199 DEFAULT_SNAPSHOT_FILE_TEXT = ("""\
200# Snapshot generated with gclient revinfo --snapshot
201solutions = [
202%(solution_list)s
203]
204""")
205
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000206 def __init__(self, root_dir, options):
207 self._root_dir = root_dir
208 self._options = options
maruel@chromium.org116704f2010-06-11 17:34:38 +0000209 self.config_content = None
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000210 self._config_dict = {}
211 self._deps_hooks = []
212
213 def SetConfig(self, content):
214 self._config_dict = {}
maruel@chromium.org116704f2010-06-11 17:34:38 +0000215 self.config_content = content
skylined@chromium.orgdf0032c2009-05-29 10:43:56 +0000216 try:
217 exec(content, self._config_dict)
218 except SyntaxError, e:
219 try:
220 # Try to construct a human readable error message
221 error_message = [
222 'There is a syntax error in your configuration file.',
223 'Line #%s, character %s:' % (e.lineno, e.offset),
224 '"%s"' % re.sub(r'[\r\n]*$', '', e.text) ]
225 except:
226 # Something went wrong, re-raise the original exception
227 raise e
228 else:
229 # Raise a new exception with the human readable message:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000230 raise gclient_utils.Error('\n'.join(error_message))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000231
232 def SaveConfig(self):
maruel@chromium.org75a59272010-06-11 22:34:03 +0000233 gclient_utils.FileWrite(os.path.join(self.root_dir(),
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000234 self._options.config_filename),
maruel@chromium.org116704f2010-06-11 17:34:38 +0000235 self.config_content)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000236
237 def _LoadConfig(self):
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000238 client_source = gclient_utils.FileRead(
maruel@chromium.org75a59272010-06-11 22:34:03 +0000239 os.path.join(self.root_dir(), self._options.config_filename))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000240 self.SetConfig(client_source)
241
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000242 def GetVar(self, key, default=None):
243 return self._config_dict.get(key, default)
244
245 @staticmethod
246 def LoadCurrentConfig(options, from_dir=None):
247 """Searches for and loads a .gclient file relative to the current working
248 dir.
249
250 Returns:
251 A dict representing the contents of the .gclient file or an empty dict if
252 the .gclient file doesn't exist.
253 """
254 if not from_dir:
255 from_dir = os.curdir
256 path = os.path.realpath(from_dir)
maruel@chromium.org0329e672009-05-13 18:41:04 +0000257 while not os.path.exists(os.path.join(path, options.config_filename)):
maruel@chromium.org55e724e2010-03-11 19:36:49 +0000258 split_path = os.path.split(path)
259 if not split_path[1]:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000260 return None
maruel@chromium.org55e724e2010-03-11 19:36:49 +0000261 path = split_path[0]
maruel@chromium.org2806acc2009-05-15 12:33:34 +0000262 client = GClient(path, options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000263 client._LoadConfig()
264 return client
265
266 def SetDefaultConfig(self, solution_name, solution_url, safesync_url):
maruel@chromium.org1f7d1182010-05-17 18:17:38 +0000267 self.SetConfig(self.DEFAULT_CLIENT_FILE_TEXT % {
gspencer@google.comdf2d5902009-09-11 22:16:21 +0000268 'solution_name': solution_name,
269 'solution_url': solution_url,
270 'safesync_url' : safesync_url,
271 })
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000272
273 def _SaveEntries(self, entries):
274 """Creates a .gclient_entries file to record the list of unique checkouts.
275
276 The .gclient_entries file lives in the same directory as .gclient.
277
278 Args:
279 entries: A sequence of solution names.
280 """
maruel@chromium.orge41f6822010-04-08 16:37:06 +0000281 # Sometimes pprint.pformat will use {', sometimes it'll use { ' ... It
282 # makes testing a bit too fun.
283 result = pprint.pformat(entries, 2)
284 if result.startswith('{\''):
maruel@chromium.org1edec4d2010-04-08 17:17:06 +0000285 result = '{ \'' + result[2:]
maruel@chromium.orge41f6822010-04-08 16:37:06 +0000286 text = "entries = \\\n" + result + '\n'
maruel@chromium.org75a59272010-06-11 22:34:03 +0000287 file_path = os.path.join(self.root_dir(), self._options.entries_filename)
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000288 gclient_utils.FileWrite(file_path, text)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000289
290 def _ReadEntries(self):
291 """Read the .gclient_entries file for the given client.
292
293 Args:
294 client: The client for which the entries file should be read.
295
296 Returns:
297 A sequence of solution names, which will be empty if there is the
298 entries file hasn't been created yet.
299 """
300 scope = {}
maruel@chromium.org75a59272010-06-11 22:34:03 +0000301 filename = os.path.join(self.root_dir(), self._options.entries_filename)
maruel@chromium.org0329e672009-05-13 18:41:04 +0000302 if not os.path.exists(filename):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000303 return []
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000304 exec(gclient_utils.FileRead(filename), scope)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000305 return scope["entries"]
306
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000307 def _ParseSolutionDeps(self, solution_name, solution_deps_content,
tony@chromium.org30ef9ae2010-04-09 02:18:05 +0000308 custom_vars, parse_hooks):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000309 """Parses the DEPS file for the specified solution.
310
311 Args:
312 solution_name: The name of the solution to query.
313 solution_deps_content: Content of the DEPS file for the solution
314 custom_vars: A dict of vars to override any vars defined in the DEPS file.
315
316 Returns:
317 A dict mapping module names (as relative paths) to URLs or an empty
318 dict if the solution does not have a DEPS file.
319 """
320 # Skip empty
321 if not solution_deps_content:
322 return {}
323 # Eval the content
324 local_scope = {}
maruel@chromium.org116704f2010-06-11 17:34:38 +0000325 var = self.VarImpl(custom_vars, local_scope)
tony@chromium.org4b5b1772010-04-08 01:52:56 +0000326 global_scope = {
327 "File": self.FileImpl,
328 "From": self.FromImpl,
329 "Var": var.Lookup,
330 "deps_os": {},
331 }
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000332 exec(solution_deps_content, global_scope, local_scope)
333 deps = local_scope.get("deps", {})
334
335 # load os specific dependencies if defined. these dependencies may
336 # override or extend the values defined by the 'deps' member.
337 if "deps_os" in local_scope:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000338 if self._options.deps_os is not None:
339 deps_to_include = self._options.deps_os.split(",")
340 if "all" in deps_to_include:
maruel@chromium.org116704f2010-06-11 17:34:38 +0000341 deps_to_include = list(set(self.DEPS_OS_CHOICES.itervalues()))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000342 else:
maruel@chromium.org116704f2010-06-11 17:34:38 +0000343 deps_to_include = [self.DEPS_OS_CHOICES.get(sys.platform, "unix")]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000344
345 deps_to_include = set(deps_to_include)
346 for deps_os_key in deps_to_include:
347 os_deps = local_scope["deps_os"].get(deps_os_key, {})
348 if len(deps_to_include) > 1:
349 # Ignore any overrides when including deps for more than one
350 # platform, so we collect the broadest set of dependencies available.
351 # We may end up with the wrong revision of something for our
352 # platform, but this is the best we can do.
353 deps.update([x for x in os_deps.items() if not x[0] in deps])
354 else:
355 deps.update(os_deps)
356
tony@chromium.org30ef9ae2010-04-09 02:18:05 +0000357 if 'hooks' in local_scope and parse_hooks:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000358 self._deps_hooks.extend(local_scope['hooks'])
359
360 # If use_relative_paths is set in the DEPS file, regenerate
361 # the dictionary using paths relative to the directory containing
362 # the DEPS file.
363 if local_scope.get('use_relative_paths'):
364 rel_deps = {}
365 for d, url in deps.items():
366 # normpath is required to allow DEPS to use .. in their
367 # dependency local path.
368 rel_deps[os.path.normpath(os.path.join(solution_name, d))] = url
369 return rel_deps
370 else:
371 return deps
372
373 def _ParseAllDeps(self, solution_urls, solution_deps_content):
374 """Parse the complete list of dependencies for the client.
375
376 Args:
377 solution_urls: A dict mapping module names (as relative paths) to URLs
378 corresponding to the solutions specified by the client. This parameter
379 is passed as an optimization.
380 solution_deps_content: A dict mapping module names to the content
381 of their DEPS files
382
383 Returns:
384 A dict mapping module names (as relative paths) to URLs corresponding
385 to the entire set of dependencies to checkout for the given client.
386
387 Raises:
388 Error: If a dependency conflicts with another dependency or of a solution.
389 """
390 deps = {}
391 for solution in self.GetVar("solutions"):
392 custom_vars = solution.get("custom_vars", {})
393 solution_deps = self._ParseSolutionDeps(
394 solution["name"],
395 solution_deps_content[solution["name"]],
tony@chromium.org30ef9ae2010-04-09 02:18:05 +0000396 custom_vars,
397 True)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000398
399 # If a line is in custom_deps, but not in the solution, we want to append
400 # this line to the solution.
401 if "custom_deps" in solution:
402 for d in solution["custom_deps"]:
403 if d not in solution_deps:
404 solution_deps[d] = solution["custom_deps"][d]
405
406 for d in solution_deps:
407 if "custom_deps" in solution and d in solution["custom_deps"]:
408 # Dependency is overriden.
409 url = solution["custom_deps"][d]
410 if url is None:
411 continue
412 else:
413 url = solution_deps[d]
414 # if we have a From reference dependent on another solution, then
415 # just skip the From reference. When we pull deps for the solution,
416 # we will take care of this dependency.
417 #
418 # If multiple solutions all have the same From reference, then we
419 # should only add one to our list of dependencies.
tony@chromium.org4b5b1772010-04-08 01:52:56 +0000420 if isinstance(url, self.FromImpl):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000421 if url.module_name in solution_urls:
422 # Already parsed.
423 continue
424 if d in deps and type(deps[d]) != str:
425 if url.module_name == deps[d].module_name:
426 continue
tony@chromium.org4b5b1772010-04-08 01:52:56 +0000427 elif isinstance(url, str):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000428 parsed_url = urlparse.urlparse(url)
429 scheme = parsed_url[0]
430 if not scheme:
431 # A relative url. Fetch the real base.
432 path = parsed_url[2]
433 if path[0] != "/":
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000434 raise gclient_utils.Error(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000435 "relative DEPS entry \"%s\" must begin with a slash" % d)
msb@chromium.orge6f78352010-01-13 17:05:33 +0000436 # Create a scm just to query the full url.
maruel@chromium.org75a59272010-06-11 22:34:03 +0000437 scm = gclient_scm.CreateSCM(solution["url"], self.root_dir(),
msb@chromium.orge6f78352010-01-13 17:05:33 +0000438 None)
439 url = scm.FullUrlForRelativeUrl(url)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000440 if d in deps and deps[d] != url:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000441 raise gclient_utils.Error(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000442 "Solutions have conflicting versions of dependency \"%s\"" % d)
443 if d in solution_urls and solution_urls[d] != url:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000444 raise gclient_utils.Error(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000445 "Dependency \"%s\" conflicts with specified solution" % d)
446 # Grab the dependency.
447 deps[d] = url
448 return deps
449
phajdan.jr@chromium.org71b40682009-07-31 23:40:09 +0000450 def _RunHookAction(self, hook_dict, matching_file_list):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000451 """Runs the action from a single hook.
452 """
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000453 logging.info(hook_dict)
454 logging.info(matching_file_list)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000455 command = hook_dict['action'][:]
456 if command[0] == 'python':
457 # If the hook specified "python" as the first item, the action is a
458 # Python script. Run it by starting a new copy of the same
459 # interpreter.
460 command[0] = sys.executable
461
phajdan.jr@chromium.org71b40682009-07-31 23:40:09 +0000462 if '$matching_files' in command:
phajdan.jr@chromium.org68f2e092009-08-06 17:05:35 +0000463 splice_index = command.index('$matching_files')
464 command[splice_index:splice_index + 1] = matching_file_list
phajdan.jr@chromium.org71b40682009-07-31 23:40:09 +0000465
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000466 # Use a discrete exit status code of 2 to indicate that a hook action
467 # failed. Users of this script may wish to treat hook action failures
468 # differently from VC failures.
maruel@chromium.org75a59272010-06-11 22:34:03 +0000469 gclient_utils.SubprocessCall(command, self.root_dir(), fail_status=2)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000470
471 def _RunHooks(self, command, file_list, is_using_git):
472 """Evaluates all hooks, running actions as needed.
473 """
474 # Hooks only run for these command types.
475 if not command in ('update', 'revert', 'runhooks'):
476 return
477
evan@chromium.org67820ef2009-07-27 17:23:00 +0000478 # Hooks only run when --nohooks is not specified
479 if self._options.nohooks:
480 return
481
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000482 # Get any hooks from the .gclient file.
483 hooks = self.GetVar("hooks", [])
484 # Add any hooks found in DEPS files.
485 hooks.extend(self._deps_hooks)
486
487 # If "--force" was specified, run all hooks regardless of what files have
488 # changed. If the user is using git, then we don't know what files have
489 # changed so we always run all hooks.
490 if self._options.force or is_using_git:
491 for hook_dict in hooks:
phajdan.jr@chromium.org71b40682009-07-31 23:40:09 +0000492 self._RunHookAction(hook_dict, [])
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000493 return
494
495 # Run hooks on the basis of whether the files from the gclient operation
496 # match each hook's pattern.
497 for hook_dict in hooks:
498 pattern = re.compile(hook_dict['pattern'])
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000499 matching_file_list = [f for f in file_list if pattern.search(f)]
phajdan.jr@chromium.org71b40682009-07-31 23:40:09 +0000500 if matching_file_list:
501 self._RunHookAction(hook_dict, matching_file_list)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000502
maruel@chromium.org918a9ae2010-05-28 15:50:30 +0000503 def _EnforceRevisions(self, solutions):
504 """Checks for revision overrides."""
505 revision_overrides = {}
maruel@chromium.org307d1792010-05-31 20:03:13 +0000506 if self._options.head:
507 return revision_overrides
508 for s in solutions:
509 if not s.get('safesync_url', None):
510 continue
511 handle = urllib.urlopen(s['safesync_url'])
512 rev = handle.read().strip()
513 handle.close()
514 if len(rev):
515 self._options.revisions.append('%s@%s' % (s['name'], rev))
516 if not self._options.revisions:
517 return revision_overrides
518 # --revision will take over safesync_url.
519 solutions_names = [s['name'] for s in solutions]
520 index = 0
521 for revision in self._options.revisions:
522 if not '@' in revision:
523 # Support for --revision 123
524 revision = '%s@%s' % (solutions_names[index], revision)
525 sol, rev = revision.split("@", 1)
526 if not sol in solutions_names:
527 #raise gclient_utils.Error('%s is not a valid solution.' % sol)
528 print >> sys.stderr, ('Please fix your script, having invalid '
529 '--revision flags will soon considered an error.')
530 else:
maruel@chromium.org918a9ae2010-05-28 15:50:30 +0000531 revision_overrides[sol] = rev
maruel@chromium.org307d1792010-05-31 20:03:13 +0000532 index += 1
maruel@chromium.org918a9ae2010-05-28 15:50:30 +0000533 return revision_overrides
534
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000535 def RunOnDeps(self, command, args):
536 """Runs a command on each dependency in a client and its dependencies.
537
538 The module's dependencies are specified in its top-level DEPS files.
539
540 Args:
541 command: The command to use (e.g., 'status' or 'diff')
542 args: list of str - extra arguments to add to the command line.
543
544 Raises:
545 Error: If the client has conflicting entries.
546 """
maruel@chromium.org116704f2010-06-11 17:34:38 +0000547 if not command in self.SUPPORTED_COMMANDS:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000548 raise gclient_utils.Error("'%s' is an unsupported command" % command)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000549
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000550 solutions = self.GetVar("solutions")
551 if not solutions:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000552 raise gclient_utils.Error("No solution specified")
maruel@chromium.org918a9ae2010-05-28 15:50:30 +0000553 revision_overrides = self._EnforceRevisions(solutions)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000554
555 # When running runhooks --force, there's no need to consult the SCM.
556 # All known hooks are expected to run unconditionally regardless of working
557 # copy state, so skip the SCM status check.
558 run_scm = not (command == 'runhooks' and self._options.force)
559
560 entries = {}
561 entries_deps_content = {}
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000562 file_list = []
563 # Run on the base solutions first.
564 for solution in solutions:
565 name = solution["name"]
maruel@chromium.org1f7d1182010-05-17 18:17:38 +0000566 deps_file = solution.get("deps_file", self.DEPS_FILE)
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000567 if '/' in deps_file or '\\' in deps_file:
568 raise gclient_utils.Error('deps_file name must not be a path, just a '
569 'filename.')
570 if name in entries:
571 raise gclient_utils.Error("solution %s specified more than once" % name)
572 url = solution["url"]
573 entries[name] = url
574 if run_scm and url:
575 self._options.revision = revision_overrides.get(name)
maruel@chromium.org75a59272010-06-11 22:34:03 +0000576 scm = gclient_scm.CreateSCM(url, self.root_dir(), name)
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000577 scm.RunCommand(command, self._options, args, file_list)
578 file_list = [os.path.join(name, f.strip()) for f in file_list]
579 self._options.revision = None
580 try:
581 deps_content = gclient_utils.FileRead(
maruel@chromium.org75a59272010-06-11 22:34:03 +0000582 os.path.join(self.root_dir(), name, deps_file))
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000583 except IOError, e:
584 if e.errno != errno.ENOENT:
585 raise
586 deps_content = ""
587 entries_deps_content[name] = deps_content
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000588
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000589 # Process the dependencies next (sort alphanumerically to ensure that
590 # containing directories get populated first and for readability)
591 deps = self._ParseAllDeps(entries, entries_deps_content)
592 deps_to_process = deps.keys()
593 deps_to_process.sort()
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000594
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000595 # First pass for direct dependencies.
596 if command == 'update' and not self._options.verbose:
597 pm = Progress('Syncing projects', len(deps_to_process))
598 for d in deps_to_process:
nasser@codeaurora.org1f7a3d12010-02-04 15:11:50 +0000599 if command == 'update' and not self._options.verbose:
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000600 pm.update()
601 if type(deps[d]) == str:
602 url = deps[d]
603 entries[d] = url
604 if run_scm:
605 self._options.revision = revision_overrides.get(d)
maruel@chromium.org75a59272010-06-11 22:34:03 +0000606 scm = gclient_scm.CreateSCM(url, self.root_dir(), d)
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000607 scm.RunCommand(command, self._options, args, file_list)
608 self._options.revision = None
609 elif isinstance(deps[d], self.FileImpl):
maruel@chromium.org491c04b2010-05-17 18:17:44 +0000610 file_dep = deps[d]
611 self._options.revision = file_dep.GetRevision()
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000612 if run_scm:
maruel@chromium.org75a59272010-06-11 22:34:03 +0000613 scm = gclient_scm.CreateSCM(file_dep.GetPath(), self.root_dir(), d)
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000614 scm.RunCommand("updatesingle", self._options,
maruel@chromium.org491c04b2010-05-17 18:17:44 +0000615 args + [file_dep.GetFilename()], file_list)
maruel@chromium.org79692d62010-05-14 18:57:13 +0000616
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000617 if command == 'update' and not self._options.verbose:
618 pm.end()
piman@chromium.org6f363722010-04-27 00:41:09 +0000619
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000620 # Second pass for inherited deps (via the From keyword)
621 for d in deps_to_process:
622 if isinstance(deps[d], self.FromImpl):
maruel@chromium.org75a59272010-06-11 22:34:03 +0000623 filename = os.path.join(self.root_dir(),
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000624 deps[d].module_name,
maruel@chromium.org1f7d1182010-05-17 18:17:38 +0000625 self.DEPS_FILE)
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000626 content = gclient_utils.FileRead(filename)
627 sub_deps = self._ParseSolutionDeps(deps[d].module_name, content, {},
628 False)
629 # Getting the URL from the sub_deps file can involve having to resolve
630 # a File() or having to resolve a relative URL. To resolve relative
631 # URLs, we need to pass in the orignal sub deps URL.
632 sub_deps_base_url = deps[deps[d].module_name]
maruel@chromium.org75a59272010-06-11 22:34:03 +0000633 url = deps[d].GetUrl(d, sub_deps_base_url, self.root_dir(), sub_deps)
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000634 entries[d] = url
635 if run_scm:
636 self._options.revision = revision_overrides.get(d)
maruel@chromium.org75a59272010-06-11 22:34:03 +0000637 scm = gclient_scm.CreateSCM(url, self.root_dir(), d)
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000638 scm.RunCommand(command, self._options, args, file_list)
639 self._options.revision = None
gspencer@google.comdf2d5902009-09-11 22:16:21 +0000640
phajdan.jr@chromium.orgd83b2b22009-08-11 15:30:55 +0000641 # Convert all absolute paths to relative.
642 for i in range(len(file_list)):
643 # TODO(phajdan.jr): We should know exactly when the paths are absolute.
644 # It depends on the command being executed (like runhooks vs sync).
645 if not os.path.isabs(file_list[i]):
646 continue
647
maruel@chromium.org75a59272010-06-11 22:34:03 +0000648 prefix = os.path.commonprefix([self.root_dir().lower(),
phajdan.jr@chromium.orgd83b2b22009-08-11 15:30:55 +0000649 file_list[i].lower()])
650 file_list[i] = file_list[i][len(prefix):]
651
652 # Strip any leading path separators.
653 while file_list[i].startswith('\\') or file_list[i].startswith('/'):
654 file_list[i] = file_list[i][1:]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000655
maruel@chromium.org75a59272010-06-11 22:34:03 +0000656 is_using_git = gclient_utils.IsUsingGit(self.root_dir(), entries.keys())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000657 self._RunHooks(command, file_list, is_using_git)
658
659 if command == 'update':
ajwong@chromium.orgcdcee802009-06-23 15:30:42 +0000660 # Notify the user if there is an orphaned entry in their working copy.
661 # Only delete the directory if there are no changes in it, and
662 # delete_unversioned_trees is set to true.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000663 prev_entries = self._ReadEntries()
664 for entry in prev_entries:
maruel@chromium.orgc5e9aec2009-08-03 18:25:56 +0000665 # Fix path separator on Windows.
666 entry_fixed = entry.replace('/', os.path.sep)
maruel@chromium.org75a59272010-06-11 22:34:03 +0000667 e_dir = os.path.join(self.root_dir(), entry_fixed)
maruel@chromium.orgc5e9aec2009-08-03 18:25:56 +0000668 # Use entry and not entry_fixed there.
maruel@chromium.org0329e672009-05-13 18:41:04 +0000669 if entry not in entries and os.path.exists(e_dir):
msb@chromium.org83017012009-09-28 18:52:12 +0000670 modified_files = False
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000671 if isinstance(prev_entries, list):
msb@chromium.org83017012009-09-28 18:52:12 +0000672 # old .gclient_entries format was list, now dict
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000673 modified_files = gclient_scm.scm.SVN.CaptureStatus(e_dir)
msb@chromium.org83017012009-09-28 18:52:12 +0000674 else:
675 file_list = []
maruel@chromium.org75a59272010-06-11 22:34:03 +0000676 scm = gclient_scm.CreateSCM(prev_entries[entry], self.root_dir(),
msb@chromium.org83017012009-09-28 18:52:12 +0000677 entry_fixed)
678 scm.status(self._options, [], file_list)
679 modified_files = file_list != []
680 if not self._options.delete_unversioned_trees or modified_files:
maruel@chromium.orgc5e9aec2009-08-03 18:25:56 +0000681 # There are modified files in this entry. Keep warning until
682 # removed.
maruel@chromium.orgc5e9aec2009-08-03 18:25:56 +0000683 print(("\nWARNING: \"%s\" is no longer part of this client. "
684 "It is recommended that you manually remove it.\n") %
685 entry_fixed)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000686 else:
687 # Delete the entry
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000688 print("\n________ deleting \'%s\' " +
maruel@chromium.org75a59272010-06-11 22:34:03 +0000689 "in \'%s\'") % (entry_fixed, self.root_dir())
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000690 gclient_utils.RemoveDirectory(e_dir)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000691 # record the current list of entries for next time
692 self._SaveEntries(entries)
maruel@chromium.org17cdf762010-05-28 17:30:52 +0000693 return 0
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000694
695 def PrintRevInfo(self):
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000696 """Output revision info mapping for the client and its dependencies.
697
698 This allows the capture of an overall "revision" for the source tree that
maruel@chromium.org918a9ae2010-05-28 15:50:30 +0000699 can be used to reproduce the same tree in the future. It is only useful for
700 "unpinned dependencies", i.e. DEPS/deps references without a svn revision
701 number or a git hash. A git branch name isn't "pinned" since the actual
702 commit can change.
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000703
704 The --snapshot option allows creating a .gclient file to reproduce the tree.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000705 """
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000706 solutions = self.GetVar("solutions")
707 if not solutions:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000708 raise gclient_utils.Error("No solution specified")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000709
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000710 # Inner helper to generate base url and rev tuple
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000711 def GetURLAndRev(name, original_url):
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000712 url, _ = gclient_utils.SplitUrlRevision(original_url)
maruel@chromium.org75a59272010-06-11 22:34:03 +0000713 scm = gclient_scm.CreateSCM(original_url, self.root_dir(), name)
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000714 return (url, scm.revinfo(self._options, [], None))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000715
maruel@chromium.orge3da35f2010-03-09 21:40:45 +0000716 # text of the snapshot gclient file
717 new_gclient = ""
718 # Dictionary of { path : SCM url } to ensure no duplicate solutions
719 solution_names = {}
nasser@codeaurora.orgde8f3522010-03-11 23:47:44 +0000720 entries = {}
721 entries_deps_content = {}
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000722 # Run on the base solutions first.
723 for solution in solutions:
maruel@chromium.orge3da35f2010-03-09 21:40:45 +0000724 # Dictionary of { path : SCM url } to describe the gclient checkout
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000725 name = solution["name"]
maruel@chromium.orge3da35f2010-03-09 21:40:45 +0000726 if name in solution_names:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000727 raise gclient_utils.Error("solution %s specified more than once" % name)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000728 (url, rev) = GetURLAndRev(name, solution["url"])
msb@chromium.org770ff9e2009-09-23 17:18:18 +0000729 entries[name] = "%s@%s" % (url, rev)
maruel@chromium.orge3da35f2010-03-09 21:40:45 +0000730 solution_names[name] = "%s@%s" % (url, rev)
maruel@chromium.org491c04b2010-05-17 18:17:44 +0000731 deps_file = solution.get("deps_file", self.DEPS_FILE)
nasser@codeaurora.org952d7c72010-03-01 20:41:01 +0000732 if '/' in deps_file or '\\' in deps_file:
733 raise gclient_utils.Error('deps_file name must not be a path, just a '
734 'filename.')
735 try:
736 deps_content = gclient_utils.FileRead(
maruel@chromium.org75a59272010-06-11 22:34:03 +0000737 os.path.join(self.root_dir(), name, deps_file))
nasser@codeaurora.org952d7c72010-03-01 20:41:01 +0000738 except IOError, e:
739 if e.errno != errno.ENOENT:
740 raise
741 deps_content = ""
742 entries_deps_content[name] = deps_content
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000743
nasser@codeaurora.orgde8f3522010-03-11 23:47:44 +0000744 # Process the dependencies next (sort alphanumerically to ensure that
745 # containing directories get populated first and for readability)
746 deps = self._ParseAllDeps(entries, entries_deps_content)
747 deps_to_process = deps.keys()
748 deps_to_process.sort()
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000749
nasser@codeaurora.orgde8f3522010-03-11 23:47:44 +0000750 # First pass for direct dependencies.
751 for d in deps_to_process:
752 if type(deps[d]) == str:
753 (url, rev) = GetURLAndRev(d, deps[d])
754 entries[d] = "%s@%s" % (url, rev)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000755
nasser@codeaurora.orgde8f3522010-03-11 23:47:44 +0000756 # Second pass for inherited deps (via the From keyword)
757 for d in deps_to_process:
tony@chromium.org4b5b1772010-04-08 01:52:56 +0000758 if isinstance(deps[d], self.FromImpl):
nasser@codeaurora.orgde8f3522010-03-11 23:47:44 +0000759 deps_parent_url = entries[deps[d].module_name]
760 if deps_parent_url.find("@") < 0:
761 raise gclient_utils.Error("From %s missing revisioned url" %
762 deps[d].module_name)
763 content = gclient_utils.FileRead(os.path.join(
maruel@chromium.org75a59272010-06-11 22:34:03 +0000764 self.root_dir(),
nasser@codeaurora.orgde8f3522010-03-11 23:47:44 +0000765 deps[d].module_name,
maruel@chromium.org491c04b2010-05-17 18:17:44 +0000766 self.DEPS_FILE))
767 sub_deps = self._ParseSolutionDeps(deps[d].module_name, content, {},
768 False)
nasser@codeaurora.orgde8f3522010-03-11 23:47:44 +0000769 (url, rev) = GetURLAndRev(d, sub_deps[d])
770 entries[d] = "%s@%s" % (url, rev)
maruel@chromium.orge3da35f2010-03-09 21:40:45 +0000771
nasser@codeaurora.orgde8f3522010-03-11 23:47:44 +0000772 # Build the snapshot configuration string
773 if self._options.snapshot:
774 url = entries.pop(name)
775 custom_deps = ",\n ".join(["\"%s\": \"%s\"" % (x, entries[x])
776 for x in sorted(entries.keys())])
maruel@chromium.orge3da35f2010-03-09 21:40:45 +0000777
maruel@chromium.org1f7d1182010-05-17 18:17:38 +0000778 new_gclient += self.DEFAULT_SNAPSHOT_SOLUTION_TEXT % {
nasser@codeaurora.orgde8f3522010-03-11 23:47:44 +0000779 'solution_name': name,
780 'solution_url': url,
781 'safesync_url' : "",
782 'solution_deps': custom_deps,
783 }
784 else:
785 print(";\n".join(["%s: %s" % (x, entries[x])
786 for x in sorted(entries.keys())]))
maruel@chromium.orge3da35f2010-03-09 21:40:45 +0000787
788 # Print the snapshot configuration file
789 if self._options.snapshot:
maruel@chromium.org491c04b2010-05-17 18:17:44 +0000790 config = self.DEFAULT_SNAPSHOT_FILE_TEXT % {'solution_list': new_gclient}
maruel@chromium.org75a59272010-06-11 22:34:03 +0000791 snapclient = GClient(self.root_dir(), self._options)
maruel@chromium.orge3da35f2010-03-09 21:40:45 +0000792 snapclient.SetConfig(config)
maruel@chromium.org116704f2010-06-11 17:34:38 +0000793 print(snapclient.config_content)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000794
maruel@chromium.org75a59272010-06-11 22:34:03 +0000795 def root_dir(self):
796 return self._root_dir
797
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000798
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.org116704f2010-06-11 17:34:38 +0000818 print(client.config_content)
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.
maruel@chromium.org116704f2010-06-11 17:34:38 +0000881 print(client.config_content)
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +0000882 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.
maruel@chromium.org116704f2010-06-11 17:34:38 +0000908 print(client.config_content)
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.org116704f2010-06-11 17:34:38 +0000925 print(client.config_content)
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."""
maruel@chromium.orgc0b8a4e2010-06-02 17:49:39 +0000941 parser.add_option("-f", "--force", action="store_true",
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000942 help="force update even for unchanged modules")
maruel@chromium.orgc0b8a4e2010-06-02 17:49:39 +0000943 parser.add_option("-n", "--nohooks", action="store_true",
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000944 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.orgc0b8a4e2010-06-02 17:49:39 +0000952 parser.add_option("-H", "--head", action="store_true",
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000953 help="skips any safesync_urls specified in "
954 "configured solutions and sync to head instead")
maruel@chromium.orgc0b8a4e2010-06-02 17:49:39 +0000955 parser.add_option("-D", "--delete_unversioned_trees", action="store_true",
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000956 help="delete any unexpected unversioned trees "
957 "that are in the checkout")
maruel@chromium.orgc0b8a4e2010-06-02 17:49:39 +0000958 parser.add_option("-R", "--reset", action="store_true",
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000959 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.orgc0b8a4e2010-06-02 17:49:39 +0000964 parser.add_option("-m", "--manually_grab_svn_rev", action="store_true",
maruel@chromium.org5ca27692010-05-26 19:32:41 +0000965 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.org116704f2010-06-11 17:34:38 +0000980 print(client.config_content)
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.org116704f2010-06-11 17:34:38 +00001001 print(client.config_content)
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.orgc0b8a4e2010-06-02 17:49:39 +00001011 parser.add_option("-n", "--nohooks", action="store_true",
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001012 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.orgc0b8a4e2010-06-02 17:49:39 +00001028 parser.add_option("-f", "--force", action="store_true", default=True,
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001029 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.org116704f2010-06-11 17:34:38 +00001037 print(client.config_content)
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):
maruel@chromium.org9eda4112010-06-11 18:56:10 +00001044 """Output revision info mapping for the client and its dependencies.
1045
1046 This allows the capture of an overall "revision" for the source tree that
1047 can be used to reproduce the same tree in the future. It is only useful for
1048 "unpinned dependencies", i.e. DEPS/deps references without a svn revision
1049 number or a git hash. A git branch name isn't "pinned" since the actual
1050 commit can change.
1051 """
1052 parser.add_option('--deps', dest='deps_os', metavar='OS_LIST',
1053 help='override deps for the specified (comma-separated) '
1054 'platform(s); \'all\' will process all deps_os '
1055 'references')
1056 parser.add_option('-s', '--snapshot', action='store_true',
1057 help='creates a snapshot .gclient file of the current '
1058 'version of all repositories to reproduce the tree, '
1059 'implies -a')
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001060 (options, args) = parser.parse_args(args)
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001061 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001062 if not client:
maruel@chromium.orge3608df2009-11-10 20:22:57 +00001063 raise gclient_utils.Error("client not configured; see 'gclient config'")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001064 client.PrintRevInfo()
maruel@chromium.org79692d62010-05-14 18:57:13 +00001065 return 0
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001066
1067
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001068def Command(name):
1069 return getattr(sys.modules[__name__], 'CMD' + name, None)
1070
1071
1072def CMDhelp(parser, args):
1073 """Prints list of commands or help for a specific command."""
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001074 (_, args) = parser.parse_args(args)
maruel@chromium.orgddff62d2010-05-17 21:02:36 +00001075 if len(args) == 1:
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001076 return Main(args + ['--help'])
maruel@chromium.orgddff62d2010-05-17 21:02:36 +00001077 parser.print_help()
1078 return 0
1079
1080
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001081def GenUsage(parser, command):
1082 """Modify an OptParse object with the function's documentation."""
1083 obj = Command(command)
1084 if command == 'help':
1085 command = '<command>'
1086 # OptParser.description prefer nicely non-formatted strings.
1087 parser.description = re.sub('[\r\n ]{2,}', ' ', obj.__doc__)
1088 usage = getattr(obj, 'usage', '')
1089 parser.set_usage('%%prog %s [options] %s' % (command, usage))
1090 parser.epilog = getattr(obj, 'epilog', None)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001091
1092
1093def Main(argv):
maruel@chromium.org5ca27692010-05-26 19:32:41 +00001094 """Doesn't parse the arguments here, just find the right subcommand to
1095 execute."""
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001096 try:
1097 # Do it late so all commands are listed.
1098 CMDhelp.usage = ('\n\nCommands are:\n' + '\n'.join([
1099 ' %-10s %s' % (fn[3:], Command(fn[3:]).__doc__.split('\n')[0].strip())
1100 for fn in dir(sys.modules[__name__]) if fn.startswith('CMD')]))
1101 parser = optparse.OptionParser(version='%prog ' + __version__)
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001102 parser.add_option('-v', '--verbose', action='count', default=0,
1103 help='Produces additional output for diagnostics. Can be '
1104 'used up to three times for more logging info.')
1105 parser.add_option('--gclientfile', dest='config_filename',
1106 default=os.environ.get('GCLIENT_FILE', '.gclient'),
1107 help='Specify an alternate %default file')
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001108 # Integrate standard options processing.
1109 old_parser = parser.parse_args
1110 def Parse(args):
1111 (options, args) = old_parser(args)
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001112 level = None
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001113 if options.verbose == 2:
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001114 level = logging.INFO
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001115 elif options.verbose > 2:
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001116 level = logging.DEBUG
1117 logging.basicConfig(level=level,
1118 format='%(module)s(%(lineno)d) %(funcName)s:%(message)s')
1119 options.entries_filename = options.config_filename + '_entries'
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001120 if not hasattr(options, 'revisions'):
1121 # GClient.RunOnDeps expects it even if not applicable.
1122 options.revisions = []
1123 if not hasattr(options, 'head'):
1124 options.head = None
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001125 if not hasattr(options, 'nohooks'):
1126 options.nohooks = True
1127 if not hasattr(options, 'deps_os'):
1128 options.deps_os = None
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001129 return (options, args)
1130 parser.parse_args = Parse
1131 # We don't want wordwrapping in epilog (usually examples)
1132 parser.format_epilog = lambda _: parser.epilog or ''
1133 if argv:
1134 command = Command(argv[0])
1135 if command:
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001136 # 'fix' the usage and the description now that we know the subcommand.
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001137 GenUsage(parser, argv[0])
1138 return command(parser, argv[1:])
1139 # Not a known command. Default to help.
1140 GenUsage(parser, 'help')
1141 return CMDhelp(parser, argv)
1142 except gclient_utils.Error, e:
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001143 print >> sys.stderr, 'Error: %s' % str(e)
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001144 return 1
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001145
1146
maruel@chromium.orgf0fc9912010-06-11 17:57:33 +00001147if '__main__' == __name__:
maruel@chromium.org6e29d572010-06-04 17:32:20 +00001148 sys.exit(Main(sys.argv[1:]))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001149
1150# vim: ts=2:sw=2:tw=80:et: