blob: 165ef392fedd210b5991f871bb24528fd51d0879 [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.org79692d62010-05-14 18:57:13 +000058__version__ = "0.3.5"
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
76# default help text
77DEFAULT_USAGE_TEXT = (
msb@chromium.orgd6504212010-01-13 17:34:31 +000078"""usage: %prog <subcommand> [options] [--] [SCM options/args...]
79a wrapper for managing a set of svn client modules and/or git repositories.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000080Version """ + __version__ + """
81
82subcommands:
83 cleanup
84 config
85 diff
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +000086 export
kbr@google.comab318592009-09-04 00:54:55 +000087 pack
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000088 revert
89 status
90 sync
91 update
92 runhooks
93 revinfo
94
msb@chromium.orgd6504212010-01-13 17:34:31 +000095Options and extra arguments can be passed to invoked SCM commands by
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000096appending them to the command line. Note that if the first such
97appended option starts with a dash (-) then the options must be
98preceded by -- to distinguish them from gclient options.
99
100For additional help on a subcommand or examples of usage, try
101 %prog help <subcommand>
102 %prog help files
103""")
104
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000105
gspencer@google.comdf2d5902009-09-11 22:16:21 +0000106DEFAULT_CLIENT_FILE_TEXT = ("""\
107# An element of this array (a "solution") describes a repository directory
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000108# that will be checked out into your working copy. Each solution may
109# optionally define additional dependencies (via its DEPS file) to be
110# checked out alongside the solution's directory. A solution may also
gspencer@google.comdf2d5902009-09-11 22:16:21 +0000111# specify custom dependencies (via the "custom_deps" property) that
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000112# override or augment the dependencies specified by the DEPS file.
gspencer@google.comdf2d5902009-09-11 22:16:21 +0000113# If a "safesync_url" is specified, it is assumed to reference the location of
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000114# a text file which contains nothing but the last known good SCM revision to
115# sync against. It is fetched if specified and used unless --head is passed
gspencer@google.comdf2d5902009-09-11 22:16:21 +0000116
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000117solutions = [
gspencer@google.comdf2d5902009-09-11 22:16:21 +0000118 { "name" : "%(solution_name)s",
119 "url" : "%(solution_url)s",
120 "custom_deps" : {
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000121 # To use the trunk of a component instead of what's in DEPS:
gspencer@google.comdf2d5902009-09-11 22:16:21 +0000122 #"component": "https://svnserver/component/trunk/",
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000123 # To exclude a component from your working copy:
gspencer@google.comdf2d5902009-09-11 22:16:21 +0000124 #"data/really_large_component": None,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000125 },
gspencer@google.comdf2d5902009-09-11 22:16:21 +0000126 "safesync_url": "%(safesync_url)s"
127 },
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000128]
129""")
130
maruel@chromium.orge3da35f2010-03-09 21:40:45 +0000131DEFAULT_SNAPSHOT_SOLUTION_TEXT = ("""\
132 { "name" : "%(solution_name)s",
133 "url" : "%(solution_url)s",
134 "custom_deps" : {
135 %(solution_deps)s,
136 },
137 "safesync_url": "%(safesync_url)s"
138 },
139""")
140
141DEFAULT_SNAPSHOT_FILE_TEXT = ("""\
142# An element of this array (a "solution") describes a repository directory
143# that will be checked out into your working copy. Each solution may
144# optionally define additional dependencies (via its DEPS file) to be
145# checked out alongside the solution's directory. A solution may also
146# specify custom dependencies (via the "custom_deps" property) that
147# override or augment the dependencies specified by the DEPS file.
148# If a "safesync_url" is specified, it is assumed to reference the location of
149# a text file which contains nothing but the last known good SCM revision to
150# sync against. It is fetched if specified and used unless --head is passed
151
152solutions = [
153%(solution_list)s
154]
155""")
156
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000157
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000158## GClient implementation.
159
160
161class GClient(object):
162 """Object that represent a gclient checkout."""
163
164 supported_commands = [
kbr@google.comab318592009-09-04 00:54:55 +0000165 'cleanup', 'diff', 'export', 'pack', 'revert', 'status', 'update',
166 'runhooks'
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000167 ]
168
169 def __init__(self, root_dir, options):
170 self._root_dir = root_dir
171 self._options = options
172 self._config_content = None
173 self._config_dict = {}
174 self._deps_hooks = []
175
176 def SetConfig(self, content):
177 self._config_dict = {}
178 self._config_content = content
skylined@chromium.orgdf0032c2009-05-29 10:43:56 +0000179 try:
180 exec(content, self._config_dict)
181 except SyntaxError, e:
182 try:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000183 __pychecker__ = 'no-objattrs'
skylined@chromium.orgdf0032c2009-05-29 10:43:56 +0000184 # Try to construct a human readable error message
185 error_message = [
186 'There is a syntax error in your configuration file.',
187 'Line #%s, character %s:' % (e.lineno, e.offset),
188 '"%s"' % re.sub(r'[\r\n]*$', '', e.text) ]
189 except:
190 # Something went wrong, re-raise the original exception
191 raise e
192 else:
193 # Raise a new exception with the human readable message:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000194 raise gclient_utils.Error('\n'.join(error_message))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000195
196 def SaveConfig(self):
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000197 gclient_utils.FileWrite(os.path.join(self._root_dir,
198 self._options.config_filename),
199 self._config_content)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000200
201 def _LoadConfig(self):
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000202 client_source = gclient_utils.FileRead(
203 os.path.join(self._root_dir, self._options.config_filename))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000204 self.SetConfig(client_source)
205
206 def ConfigContent(self):
207 return self._config_content
208
209 def GetVar(self, key, default=None):
210 return self._config_dict.get(key, default)
211
212 @staticmethod
213 def LoadCurrentConfig(options, from_dir=None):
214 """Searches for and loads a .gclient file relative to the current working
215 dir.
216
217 Returns:
218 A dict representing the contents of the .gclient file or an empty dict if
219 the .gclient file doesn't exist.
220 """
221 if not from_dir:
222 from_dir = os.curdir
223 path = os.path.realpath(from_dir)
maruel@chromium.org0329e672009-05-13 18:41:04 +0000224 while not os.path.exists(os.path.join(path, options.config_filename)):
maruel@chromium.org55e724e2010-03-11 19:36:49 +0000225 split_path = os.path.split(path)
226 if not split_path[1]:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000227 return None
maruel@chromium.org55e724e2010-03-11 19:36:49 +0000228 path = split_path[0]
maruel@chromium.org2806acc2009-05-15 12:33:34 +0000229 client = GClient(path, options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000230 client._LoadConfig()
231 return client
232
233 def SetDefaultConfig(self, solution_name, solution_url, safesync_url):
gspencer@google.comdf2d5902009-09-11 22:16:21 +0000234 self.SetConfig(DEFAULT_CLIENT_FILE_TEXT % {
235 'solution_name': solution_name,
236 'solution_url': solution_url,
237 'safesync_url' : safesync_url,
238 })
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000239
240 def _SaveEntries(self, entries):
241 """Creates a .gclient_entries file to record the list of unique checkouts.
242
243 The .gclient_entries file lives in the same directory as .gclient.
244
245 Args:
246 entries: A sequence of solution names.
247 """
maruel@chromium.orge41f6822010-04-08 16:37:06 +0000248 # Sometimes pprint.pformat will use {', sometimes it'll use { ' ... It
249 # makes testing a bit too fun.
250 result = pprint.pformat(entries, 2)
251 if result.startswith('{\''):
maruel@chromium.org1edec4d2010-04-08 17:17:06 +0000252 result = '{ \'' + result[2:]
maruel@chromium.orge41f6822010-04-08 16:37:06 +0000253 text = "entries = \\\n" + result + '\n'
msb@chromium.org2e38de72009-09-28 17:04:47 +0000254 file_path = os.path.join(self._root_dir, self._options.entries_filename)
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000255 gclient_utils.FileWrite(file_path, text)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000256
257 def _ReadEntries(self):
258 """Read the .gclient_entries file for the given client.
259
260 Args:
261 client: The client for which the entries file should be read.
262
263 Returns:
264 A sequence of solution names, which will be empty if there is the
265 entries file hasn't been created yet.
266 """
267 scope = {}
268 filename = os.path.join(self._root_dir, self._options.entries_filename)
maruel@chromium.org0329e672009-05-13 18:41:04 +0000269 if not os.path.exists(filename):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000270 return []
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000271 exec(gclient_utils.FileRead(filename), scope)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000272 return scope["entries"]
273
274 class FromImpl:
275 """Used to implement the From syntax."""
276
tony@chromium.org30ef9ae2010-04-09 02:18:05 +0000277 def __init__(self, module_name, sub_target_name=None):
278 """module_name is the dep module we want to include from. It can also be
279 the name of a subdirectory to include from.
280
281 sub_target_name is an optional parameter if the module name in the other
282 DEPS file is different. E.g., you might want to map src/net to net."""
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000283 self.module_name = module_name
tony@chromium.org30ef9ae2010-04-09 02:18:05 +0000284 self.sub_target_name = sub_target_name
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000285
286 def __str__(self):
tony@chromium.org30ef9ae2010-04-09 02:18:05 +0000287 return 'From(%s, %s)' % (repr(self.module_name),
288 repr(self.sub_target_name))
289
290 def GetUrl(self, target_name, sub_deps_base_url, root_dir, sub_deps):
291 """Resolve the URL for this From entry."""
292 sub_deps_target_name = target_name
293 if self.sub_target_name:
294 sub_deps_target_name = self.sub_target_name
295 url = sub_deps[sub_deps_target_name]
296 if url.startswith('/'):
297 # If it's a relative URL, we need to resolve the URL relative to the
298 # sub deps base URL.
299 if not isinstance(sub_deps_base_url, basestring):
300 sub_deps_base_url = sub_deps_base_url.GetPath()
301 scm = gclient_scm.CreateSCM(sub_deps_base_url, root_dir,
302 None)
303 url = scm.FullUrlForRelativeUrl(url)
304 return url
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000305
tony@chromium.org4b5b1772010-04-08 01:52:56 +0000306 class FileImpl:
307 """Used to implement the File('') syntax which lets you sync a single file
308 from an SVN repo."""
309
310 def __init__(self, file_location):
311 self.file_location = file_location
312
313 def __str__(self):
314 return 'File("%s")' % self.file_location
315
316 def GetPath(self):
317 return os.path.split(self.file_location)[0]
318
319 def GetFilename(self):
320 rev_tokens = self.file_location.split('@')
321 return os.path.split(rev_tokens[0])[1]
322
323 def GetRevision(self):
324 rev_tokens = self.file_location.split('@')
325 if len(rev_tokens) > 1:
326 return rev_tokens[1]
327 return None
328
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000329 class _VarImpl:
330 def __init__(self, custom_vars, local_scope):
331 self._custom_vars = custom_vars
332 self._local_scope = local_scope
333
334 def Lookup(self, var_name):
335 """Implements the Var syntax."""
336 if var_name in self._custom_vars:
337 return self._custom_vars[var_name]
338 elif var_name in self._local_scope.get("vars", {}):
339 return self._local_scope["vars"][var_name]
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000340 raise gclient_utils.Error("Var is not defined: %s" % var_name)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000341
342 def _ParseSolutionDeps(self, solution_name, solution_deps_content,
tony@chromium.org30ef9ae2010-04-09 02:18:05 +0000343 custom_vars, parse_hooks):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000344 """Parses the DEPS file for the specified solution.
345
346 Args:
347 solution_name: The name of the solution to query.
348 solution_deps_content: Content of the DEPS file for the solution
349 custom_vars: A dict of vars to override any vars defined in the DEPS file.
350
351 Returns:
352 A dict mapping module names (as relative paths) to URLs or an empty
353 dict if the solution does not have a DEPS file.
354 """
355 # Skip empty
356 if not solution_deps_content:
357 return {}
358 # Eval the content
359 local_scope = {}
360 var = self._VarImpl(custom_vars, local_scope)
tony@chromium.org4b5b1772010-04-08 01:52:56 +0000361 global_scope = {
362 "File": self.FileImpl,
363 "From": self.FromImpl,
364 "Var": var.Lookup,
365 "deps_os": {},
366 }
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000367 exec(solution_deps_content, global_scope, local_scope)
368 deps = local_scope.get("deps", {})
369
370 # load os specific dependencies if defined. these dependencies may
371 # override or extend the values defined by the 'deps' member.
372 if "deps_os" in local_scope:
373 deps_os_choices = {
374 "win32": "win",
375 "win": "win",
376 "cygwin": "win",
377 "darwin": "mac",
378 "mac": "mac",
379 "unix": "unix",
380 "linux": "unix",
381 "linux2": "unix",
382 }
383
384 if self._options.deps_os is not None:
385 deps_to_include = self._options.deps_os.split(",")
386 if "all" in deps_to_include:
maruel@chromium.org55e724e2010-03-11 19:36:49 +0000387 deps_to_include = list(set(deps_os_choices.itervalues()))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000388 else:
389 deps_to_include = [deps_os_choices.get(self._options.platform, "unix")]
390
391 deps_to_include = set(deps_to_include)
392 for deps_os_key in deps_to_include:
393 os_deps = local_scope["deps_os"].get(deps_os_key, {})
394 if len(deps_to_include) > 1:
395 # Ignore any overrides when including deps for more than one
396 # platform, so we collect the broadest set of dependencies available.
397 # We may end up with the wrong revision of something for our
398 # platform, but this is the best we can do.
399 deps.update([x for x in os_deps.items() if not x[0] in deps])
400 else:
401 deps.update(os_deps)
402
tony@chromium.org30ef9ae2010-04-09 02:18:05 +0000403 if 'hooks' in local_scope and parse_hooks:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000404 self._deps_hooks.extend(local_scope['hooks'])
405
406 # If use_relative_paths is set in the DEPS file, regenerate
407 # the dictionary using paths relative to the directory containing
408 # the DEPS file.
409 if local_scope.get('use_relative_paths'):
410 rel_deps = {}
411 for d, url in deps.items():
412 # normpath is required to allow DEPS to use .. in their
413 # dependency local path.
414 rel_deps[os.path.normpath(os.path.join(solution_name, d))] = url
415 return rel_deps
416 else:
417 return deps
418
419 def _ParseAllDeps(self, solution_urls, solution_deps_content):
420 """Parse the complete list of dependencies for the client.
421
422 Args:
423 solution_urls: A dict mapping module names (as relative paths) to URLs
424 corresponding to the solutions specified by the client. This parameter
425 is passed as an optimization.
426 solution_deps_content: A dict mapping module names to the content
427 of their DEPS files
428
429 Returns:
430 A dict mapping module names (as relative paths) to URLs corresponding
431 to the entire set of dependencies to checkout for the given client.
432
433 Raises:
434 Error: If a dependency conflicts with another dependency or of a solution.
435 """
436 deps = {}
437 for solution in self.GetVar("solutions"):
438 custom_vars = solution.get("custom_vars", {})
439 solution_deps = self._ParseSolutionDeps(
440 solution["name"],
441 solution_deps_content[solution["name"]],
tony@chromium.org30ef9ae2010-04-09 02:18:05 +0000442 custom_vars,
443 True)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000444
445 # If a line is in custom_deps, but not in the solution, we want to append
446 # this line to the solution.
447 if "custom_deps" in solution:
448 for d in solution["custom_deps"]:
449 if d not in solution_deps:
450 solution_deps[d] = solution["custom_deps"][d]
451
452 for d in solution_deps:
453 if "custom_deps" in solution and d in solution["custom_deps"]:
454 # Dependency is overriden.
455 url = solution["custom_deps"][d]
456 if url is None:
457 continue
458 else:
459 url = solution_deps[d]
460 # if we have a From reference dependent on another solution, then
461 # just skip the From reference. When we pull deps for the solution,
462 # we will take care of this dependency.
463 #
464 # If multiple solutions all have the same From reference, then we
465 # should only add one to our list of dependencies.
tony@chromium.org4b5b1772010-04-08 01:52:56 +0000466 if isinstance(url, self.FromImpl):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000467 if url.module_name in solution_urls:
468 # Already parsed.
469 continue
470 if d in deps and type(deps[d]) != str:
471 if url.module_name == deps[d].module_name:
472 continue
tony@chromium.org4b5b1772010-04-08 01:52:56 +0000473 elif isinstance(url, str):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000474 parsed_url = urlparse.urlparse(url)
475 scheme = parsed_url[0]
476 if not scheme:
477 # A relative url. Fetch the real base.
478 path = parsed_url[2]
479 if path[0] != "/":
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000480 raise gclient_utils.Error(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000481 "relative DEPS entry \"%s\" must begin with a slash" % d)
msb@chromium.orge6f78352010-01-13 17:05:33 +0000482 # Create a scm just to query the full url.
483 scm = gclient_scm.CreateSCM(solution["url"], self._root_dir,
484 None)
485 url = scm.FullUrlForRelativeUrl(url)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000486 if d in deps and deps[d] != url:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000487 raise gclient_utils.Error(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000488 "Solutions have conflicting versions of dependency \"%s\"" % d)
489 if d in solution_urls and solution_urls[d] != url:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000490 raise gclient_utils.Error(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000491 "Dependency \"%s\" conflicts with specified solution" % d)
492 # Grab the dependency.
493 deps[d] = url
494 return deps
495
phajdan.jr@chromium.org71b40682009-07-31 23:40:09 +0000496 def _RunHookAction(self, hook_dict, matching_file_list):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000497 """Runs the action from a single hook.
498 """
499 command = hook_dict['action'][:]
500 if command[0] == 'python':
501 # If the hook specified "python" as the first item, the action is a
502 # Python script. Run it by starting a new copy of the same
503 # interpreter.
504 command[0] = sys.executable
505
phajdan.jr@chromium.org71b40682009-07-31 23:40:09 +0000506 if '$matching_files' in command:
phajdan.jr@chromium.org68f2e092009-08-06 17:05:35 +0000507 splice_index = command.index('$matching_files')
508 command[splice_index:splice_index + 1] = matching_file_list
phajdan.jr@chromium.org71b40682009-07-31 23:40:09 +0000509
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000510 # Use a discrete exit status code of 2 to indicate that a hook action
511 # failed. Users of this script may wish to treat hook action failures
512 # differently from VC failures.
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000513 gclient_utils.SubprocessCall(command, self._root_dir, fail_status=2)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000514
515 def _RunHooks(self, command, file_list, is_using_git):
516 """Evaluates all hooks, running actions as needed.
517 """
518 # Hooks only run for these command types.
519 if not command in ('update', 'revert', 'runhooks'):
520 return
521
evan@chromium.org67820ef2009-07-27 17:23:00 +0000522 # Hooks only run when --nohooks is not specified
523 if self._options.nohooks:
524 return
525
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000526 # Get any hooks from the .gclient file.
527 hooks = self.GetVar("hooks", [])
528 # Add any hooks found in DEPS files.
529 hooks.extend(self._deps_hooks)
530
531 # If "--force" was specified, run all hooks regardless of what files have
532 # changed. If the user is using git, then we don't know what files have
533 # changed so we always run all hooks.
534 if self._options.force or is_using_git:
535 for hook_dict in hooks:
phajdan.jr@chromium.org71b40682009-07-31 23:40:09 +0000536 self._RunHookAction(hook_dict, [])
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000537 return
538
539 # Run hooks on the basis of whether the files from the gclient operation
540 # match each hook's pattern.
541 for hook_dict in hooks:
542 pattern = re.compile(hook_dict['pattern'])
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000543 matching_file_list = [f for f in file_list if pattern.search(f)]
phajdan.jr@chromium.org71b40682009-07-31 23:40:09 +0000544 if matching_file_list:
545 self._RunHookAction(hook_dict, matching_file_list)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000546
547 def RunOnDeps(self, command, args):
548 """Runs a command on each dependency in a client and its dependencies.
549
550 The module's dependencies are specified in its top-level DEPS files.
551
552 Args:
553 command: The command to use (e.g., 'status' or 'diff')
554 args: list of str - extra arguments to add to the command line.
555
556 Raises:
557 Error: If the client has conflicting entries.
558 """
559 if not command in self.supported_commands:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000560 raise gclient_utils.Error("'%s' is an unsupported command" % command)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000561
562 # Check for revision overrides.
563 revision_overrides = {}
564 for revision in self._options.revisions:
565 if revision.find("@") == -1:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000566 raise gclient_utils.Error(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000567 "Specify the full dependency when specifying a revision number.")
568 revision_elem = revision.split("@")
569 # Disallow conflicting revs
570 if revision_overrides.has_key(revision_elem[0]) and \
571 revision_overrides[revision_elem[0]] != revision_elem[1]:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000572 raise gclient_utils.Error(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000573 "Conflicting revision numbers specified.")
574 revision_overrides[revision_elem[0]] = revision_elem[1]
575
576 solutions = self.GetVar("solutions")
577 if not solutions:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000578 raise gclient_utils.Error("No solution specified")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000579
580 # When running runhooks --force, there's no need to consult the SCM.
581 # All known hooks are expected to run unconditionally regardless of working
582 # copy state, so skip the SCM status check.
583 run_scm = not (command == 'runhooks' and self._options.force)
584
585 entries = {}
586 entries_deps_content = {}
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000587 file_list = []
588 # Run on the base solutions first.
589 for solution in solutions:
590 name = solution["name"]
591 deps_file = solution.get("deps_file", self._options.deps_file)
592 if '/' in deps_file or '\\' in deps_file:
593 raise gclient_utils.Error('deps_file name must not be a path, just a '
594 'filename.')
595 if name in entries:
596 raise gclient_utils.Error("solution %s specified more than once" % name)
597 url = solution["url"]
598 entries[name] = url
599 if run_scm and url:
600 self._options.revision = revision_overrides.get(name)
601 scm = gclient_scm.CreateSCM(url, self._root_dir, name)
602 scm.RunCommand(command, self._options, args, file_list)
603 file_list = [os.path.join(name, f.strip()) for f in file_list]
604 self._options.revision = None
605 try:
606 deps_content = gclient_utils.FileRead(
607 os.path.join(self._root_dir, name, deps_file))
608 except IOError, e:
609 if e.errno != errno.ENOENT:
610 raise
611 deps_content = ""
612 entries_deps_content[name] = deps_content
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000613
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000614 # Process the dependencies next (sort alphanumerically to ensure that
615 # containing directories get populated first and for readability)
616 deps = self._ParseAllDeps(entries, entries_deps_content)
617 deps_to_process = deps.keys()
618 deps_to_process.sort()
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000619
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000620 # First pass for direct dependencies.
621 if command == 'update' and not self._options.verbose:
622 pm = Progress('Syncing projects', len(deps_to_process))
623 for d in deps_to_process:
nasser@codeaurora.org1f7a3d12010-02-04 15:11:50 +0000624 if command == 'update' and not self._options.verbose:
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000625 pm.update()
626 if type(deps[d]) == str:
627 url = deps[d]
628 entries[d] = url
629 if run_scm:
630 self._options.revision = revision_overrides.get(d)
631 scm = gclient_scm.CreateSCM(url, self._root_dir, d)
632 scm.RunCommand(command, self._options, args, file_list)
633 self._options.revision = None
634 elif isinstance(deps[d], self.FileImpl):
635 file = deps[d]
636 self._options.revision = file.GetRevision()
637 if run_scm:
638 scm = gclient_scm.CreateSCM(file.GetPath(), self._root_dir, d)
639 scm.RunCommand("updatesingle", self._options,
640 args + [file.GetFilename()], file_list)
maruel@chromium.org79692d62010-05-14 18:57:13 +0000641
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000642 if command == 'update' and not self._options.verbose:
643 pm.end()
piman@chromium.org6f363722010-04-27 00:41:09 +0000644
tony@chromium.orgd2e92562010-04-27 01:55:18 +0000645 # Second pass for inherited deps (via the From keyword)
646 for d in deps_to_process:
647 if isinstance(deps[d], self.FromImpl):
648 filename = os.path.join(self._root_dir,
649 deps[d].module_name,
650 self._options.deps_file)
651 content = gclient_utils.FileRead(filename)
652 sub_deps = self._ParseSolutionDeps(deps[d].module_name, content, {},
653 False)
654 # Getting the URL from the sub_deps file can involve having to resolve
655 # a File() or having to resolve a relative URL. To resolve relative
656 # URLs, we need to pass in the orignal sub deps URL.
657 sub_deps_base_url = deps[deps[d].module_name]
658 url = deps[d].GetUrl(d, sub_deps_base_url, self._root_dir, sub_deps)
659 entries[d] = url
660 if run_scm:
661 self._options.revision = revision_overrides.get(d)
662 scm = gclient_scm.CreateSCM(url, self._root_dir, d)
663 scm.RunCommand(command, self._options, args, file_list)
664 self._options.revision = None
gspencer@google.comdf2d5902009-09-11 22:16:21 +0000665
phajdan.jr@chromium.orgd83b2b22009-08-11 15:30:55 +0000666 # Convert all absolute paths to relative.
667 for i in range(len(file_list)):
668 # TODO(phajdan.jr): We should know exactly when the paths are absolute.
669 # It depends on the command being executed (like runhooks vs sync).
670 if not os.path.isabs(file_list[i]):
671 continue
672
673 prefix = os.path.commonprefix([self._root_dir.lower(),
674 file_list[i].lower()])
675 file_list[i] = file_list[i][len(prefix):]
676
677 # Strip any leading path separators.
678 while file_list[i].startswith('\\') or file_list[i].startswith('/'):
679 file_list[i] = file_list[i][1:]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000680
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000681 is_using_git = gclient_utils.IsUsingGit(self._root_dir, entries.keys())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000682 self._RunHooks(command, file_list, is_using_git)
683
684 if command == 'update':
ajwong@chromium.orgcdcee802009-06-23 15:30:42 +0000685 # Notify the user if there is an orphaned entry in their working copy.
686 # Only delete the directory if there are no changes in it, and
687 # delete_unversioned_trees is set to true.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000688 prev_entries = self._ReadEntries()
689 for entry in prev_entries:
maruel@chromium.orgc5e9aec2009-08-03 18:25:56 +0000690 # Fix path separator on Windows.
691 entry_fixed = entry.replace('/', os.path.sep)
692 e_dir = os.path.join(self._root_dir, entry_fixed)
693 # Use entry and not entry_fixed there.
maruel@chromium.org0329e672009-05-13 18:41:04 +0000694 if entry not in entries and os.path.exists(e_dir):
msb@chromium.org83017012009-09-28 18:52:12 +0000695 modified_files = False
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000696 if isinstance(prev_entries, list):
msb@chromium.org83017012009-09-28 18:52:12 +0000697 # old .gclient_entries format was list, now dict
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000698 modified_files = gclient_scm.scm.SVN.CaptureStatus(e_dir)
msb@chromium.org83017012009-09-28 18:52:12 +0000699 else:
700 file_list = []
701 scm = gclient_scm.CreateSCM(prev_entries[entry], self._root_dir,
702 entry_fixed)
703 scm.status(self._options, [], file_list)
704 modified_files = file_list != []
705 if not self._options.delete_unversioned_trees or modified_files:
maruel@chromium.orgc5e9aec2009-08-03 18:25:56 +0000706 # There are modified files in this entry. Keep warning until
707 # removed.
maruel@chromium.orgc5e9aec2009-08-03 18:25:56 +0000708 print(("\nWARNING: \"%s\" is no longer part of this client. "
709 "It is recommended that you manually remove it.\n") %
710 entry_fixed)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000711 else:
712 # Delete the entry
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000713 print("\n________ deleting \'%s\' " +
maruel@chromium.orgc5e9aec2009-08-03 18:25:56 +0000714 "in \'%s\'") % (entry_fixed, self._root_dir)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000715 gclient_utils.RemoveDirectory(e_dir)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000716 # record the current list of entries for next time
717 self._SaveEntries(entries)
718
719 def PrintRevInfo(self):
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000720 """Output revision info mapping for the client and its dependencies.
721
722 This allows the capture of an overall "revision" for the source tree that
723 can be used to reproduce the same tree in the future. The actual output
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000724 contains enough information (source paths, svn server urls and revisions)
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000725 that it can be used either to generate external svn/git commands (without
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000726 gclient) or as input to gclient's --rev option (with some massaging of
727 the data).
728
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000729 Since we care about the revision of the current source tree, for git
730 repositories this command uses the revision of the HEAD. For subversion we
731 use BASE.
732
733 The --snapshot option allows creating a .gclient file to reproduce the tree.
734
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000735 Raises:
736 Error: If the client has conflicting entries.
737 """
738 # Check for revision overrides.
739 revision_overrides = {}
740 for revision in self._options.revisions:
741 if revision.find("@") < 0:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000742 raise gclient_utils.Error(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000743 "Specify the full dependency when specifying a revision number.")
744 revision_elem = revision.split("@")
745 # Disallow conflicting revs
746 if revision_overrides.has_key(revision_elem[0]) and \
747 revision_overrides[revision_elem[0]] != revision_elem[1]:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000748 raise gclient_utils.Error(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000749 "Conflicting revision numbers specified.")
750 revision_overrides[revision_elem[0]] = revision_elem[1]
751
752 solutions = self.GetVar("solutions")
753 if not solutions:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000754 raise gclient_utils.Error("No solution specified")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000755
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000756 # Inner helper to generate base url and rev tuple
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000757 def GetURLAndRev(name, original_url):
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000758 url, _ = gclient_utils.SplitUrlRevision(original_url)
759 scm = gclient_scm.CreateSCM(original_url, self._root_dir, name)
760 return (url, scm.revinfo(self._options, [], None))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000761
maruel@chromium.orge3da35f2010-03-09 21:40:45 +0000762 # text of the snapshot gclient file
763 new_gclient = ""
764 # Dictionary of { path : SCM url } to ensure no duplicate solutions
765 solution_names = {}
nasser@codeaurora.orgde8f3522010-03-11 23:47:44 +0000766 entries = {}
767 entries_deps_content = {}
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000768 # Run on the base solutions first.
769 for solution in solutions:
maruel@chromium.orge3da35f2010-03-09 21:40:45 +0000770 # Dictionary of { path : SCM url } to describe the gclient checkout
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000771 name = solution["name"]
maruel@chromium.orge3da35f2010-03-09 21:40:45 +0000772 if name in solution_names:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000773 raise gclient_utils.Error("solution %s specified more than once" % name)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000774 (url, rev) = GetURLAndRev(name, solution["url"])
msb@chromium.org770ff9e2009-09-23 17:18:18 +0000775 entries[name] = "%s@%s" % (url, rev)
maruel@chromium.orge3da35f2010-03-09 21:40:45 +0000776 solution_names[name] = "%s@%s" % (url, rev)
nasser@codeaurora.org952d7c72010-03-01 20:41:01 +0000777 deps_file = solution.get("deps_file", self._options.deps_file)
778 if '/' in deps_file or '\\' in deps_file:
779 raise gclient_utils.Error('deps_file name must not be a path, just a '
780 'filename.')
781 try:
782 deps_content = gclient_utils.FileRead(
783 os.path.join(self._root_dir, name, deps_file))
784 except IOError, e:
785 if e.errno != errno.ENOENT:
786 raise
787 deps_content = ""
788 entries_deps_content[name] = deps_content
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000789
nasser@codeaurora.orgde8f3522010-03-11 23:47:44 +0000790 # Process the dependencies next (sort alphanumerically to ensure that
791 # containing directories get populated first and for readability)
792 deps = self._ParseAllDeps(entries, entries_deps_content)
793 deps_to_process = deps.keys()
794 deps_to_process.sort()
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000795
nasser@codeaurora.orgde8f3522010-03-11 23:47:44 +0000796 # First pass for direct dependencies.
797 for d in deps_to_process:
798 if type(deps[d]) == str:
799 (url, rev) = GetURLAndRev(d, deps[d])
800 entries[d] = "%s@%s" % (url, rev)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000801
nasser@codeaurora.orgde8f3522010-03-11 23:47:44 +0000802 # Second pass for inherited deps (via the From keyword)
803 for d in deps_to_process:
tony@chromium.org4b5b1772010-04-08 01:52:56 +0000804 if isinstance(deps[d], self.FromImpl):
nasser@codeaurora.orgde8f3522010-03-11 23:47:44 +0000805 deps_parent_url = entries[deps[d].module_name]
806 if deps_parent_url.find("@") < 0:
807 raise gclient_utils.Error("From %s missing revisioned url" %
808 deps[d].module_name)
809 content = gclient_utils.FileRead(os.path.join(
810 self._root_dir,
811 deps[d].module_name,
812 self._options.deps_file))
813 sub_deps = self._ParseSolutionDeps(deps[d].module_name, content, {})
814 (url, rev) = GetURLAndRev(d, sub_deps[d])
815 entries[d] = "%s@%s" % (url, rev)
maruel@chromium.orge3da35f2010-03-09 21:40:45 +0000816
nasser@codeaurora.orgde8f3522010-03-11 23:47:44 +0000817 # Build the snapshot configuration string
818 if self._options.snapshot:
819 url = entries.pop(name)
820 custom_deps = ",\n ".join(["\"%s\": \"%s\"" % (x, entries[x])
821 for x in sorted(entries.keys())])
maruel@chromium.orge3da35f2010-03-09 21:40:45 +0000822
nasser@codeaurora.orgde8f3522010-03-11 23:47:44 +0000823 new_gclient += DEFAULT_SNAPSHOT_SOLUTION_TEXT % {
824 'solution_name': name,
825 'solution_url': url,
826 'safesync_url' : "",
827 'solution_deps': custom_deps,
828 }
829 else:
830 print(";\n".join(["%s: %s" % (x, entries[x])
831 for x in sorted(entries.keys())]))
maruel@chromium.orge3da35f2010-03-09 21:40:45 +0000832
833 # Print the snapshot configuration file
834 if self._options.snapshot:
835 config = DEFAULT_SNAPSHOT_FILE_TEXT % {'solution_list': new_gclient}
836 snapclient = GClient(self._root_dir, self._options)
837 snapclient.SetConfig(config)
838 print(snapclient._config_content)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000839
840
841## gclient commands.
842
843
maruel@chromium.org79692d62010-05-14 18:57:13 +0000844def CMDcleanup(options, args):
845 """Clean up all working copies, using 'svn cleanup' for each module.
846Additional options and args may be passed to 'svn cleanup'.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000847
maruel@chromium.org79692d62010-05-14 18:57:13 +0000848usage: cleanup [options] [--] [svn cleanup args/options]
849
850Valid options:
851 --verbose : output additional diagnostics
852"""
maruel@chromium.org2806acc2009-05-15 12:33:34 +0000853 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000854 if not client:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000855 raise gclient_utils.Error("client not configured; see 'gclient config'")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000856 if options.verbose:
857 # Print out the .gclient file. This is longer than if we just printed the
858 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000859 print(client.ConfigContent())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000860 return client.RunOnDeps('cleanup', args)
861
862
maruel@chromium.org79692d62010-05-14 18:57:13 +0000863def CMDconfig(options, args):
864 """Create a .gclient file in the current directory; this
865specifies the configuration for further commands. After update/sync,
866top-level DEPS files in each module are read to determine dependent
867modules to operate on as well. If optional [url] parameter is
868provided, then configuration is read from a specified Subversion server
869URL. Otherwise, a --spec option must be provided. A --name option overrides
870the default name for the solutions.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000871
maruel@chromium.org79692d62010-05-14 18:57:13 +0000872usage: config [option | url] [safesync url]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000873
maruel@chromium.org79692d62010-05-14 18:57:13 +0000874Valid options:
875 --name path : alternate relative path for the solution
876 --spec=GCLIENT_SPEC : contents of .gclient are read from string parameter.
877 *Note that due to Cygwin/Python brokenness, it
878 probably can't contain any newlines.*
879
880Examples:
881 gclient config https://gclient.googlecode.com/svn/trunk/gclient
882 configure a new client to check out gclient.py tool sources
883 gclient config --name tools https://gclient.googlecode.com/svn/trunk/gclient
884 gclient config --spec='solutions=[{"name":"gclient",
885 '"url":"https://gclient.googlecode.com/svn/trunk/gclient",'
886 '"custom_deps":{}}]'
887"""
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000888 if len(args) < 1 and not options.spec:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000889 raise gclient_utils.Error("required argument missing; see 'gclient help "
890 "config'")
maruel@chromium.org0329e672009-05-13 18:41:04 +0000891 if os.path.exists(options.config_filename):
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000892 raise gclient_utils.Error("%s file already exists in the current directory"
893 % options.config_filename)
maruel@chromium.org2806acc2009-05-15 12:33:34 +0000894 client = GClient('.', options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000895 if options.spec:
896 client.SetConfig(options.spec)
897 else:
maruel@chromium.org1ab7ffc2009-06-03 17:21:37 +0000898 base_url = args[0].rstrip('/')
iposva@chromium.org8cf7a392010-04-07 17:20:26 +0000899 if not options.name:
900 name = base_url.split("/")[-1]
901 else:
902 # specify an alternate relpath for the given URL.
903 name = options.name
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000904 safesync_url = ""
905 if len(args) > 1:
906 safesync_url = args[1]
907 client.SetDefaultConfig(name, base_url, safesync_url)
908 client.SaveConfig()
maruel@chromium.org79692d62010-05-14 18:57:13 +0000909 return 0
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000910
911
maruel@chromium.org79692d62010-05-14 18:57:13 +0000912def CMDexport(options, args):
913 """Wrapper for svn export for all managed directories
914"""
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +0000915 if len(args) != 1:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000916 raise gclient_utils.Error("Need directory name")
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +0000917 client = GClient.LoadCurrentConfig(options)
918
919 if not client:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000920 raise gclient_utils.Error("client not configured; see 'gclient config'")
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +0000921
922 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.
925 print(client.ConfigContent())
926 return client.RunOnDeps('export', args)
927
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000928
maruel@chromium.org79692d62010-05-14 18:57:13 +0000929def CMDhelp(options, args):
930 """Describe the usage of this program or its subcommands.
931
932usage: help [options] [subcommand]
933
934Valid options:
935 --verbose : output additional diagnostics
936"""
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000937 __pychecker__ = 'unusednames=options'
maruel@chromium.org79692d62010-05-14 18:57:13 +0000938 module = sys.modules[__name__]
939 commands = [x[3:] for x in dir(module) if x.startswith('CMD')]
940 if len(args) == 1 and args[0] in commands:
941 print getattr(module, 'CMD' + args[0]).__doc__
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000942 else:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000943 raise gclient_utils.Error("unknown subcommand '%s'; see 'gclient help'" %
944 args[0])
maruel@chromium.org79692d62010-05-14 18:57:13 +0000945 return 0
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000946
947
maruel@chromium.org79692d62010-05-14 18:57:13 +0000948def CMDpack(options, args):
949 """Generate a patch which can be applied at the root of the tree.
950Internally, runs 'svn diff' on each checked out module and
951dependencies, and performs minimal postprocessing of the output. The
952resulting patch is printed to stdout and can be applied to a freshly
953checked out tree via 'patch -p0 < patchfile'. Additional args and
954options to 'svn diff' can be passed after gclient options.
kbr@google.comab318592009-09-04 00:54:55 +0000955
maruel@chromium.org79692d62010-05-14 18:57:13 +0000956usage: pack [options] [--] [svn args/options]
957
958Valid options:
959 --verbose : output additional diagnostics
960
961Examples:
962 gclient pack > patch.txt
963 generate simple patch for configured client and dependences
964 gclient pack -- -x -b > patch.txt
965 generate patch using 'svn diff -x -b' to suppress
966 whitespace-only differences
967 gclient pack -- -r HEAD -x -b > patch.txt
968 generate patch, diffing each file versus the latest version of
969 each module
970"""
kbr@google.comab318592009-09-04 00:54:55 +0000971 client = GClient.LoadCurrentConfig(options)
972 if not client:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000973 raise gclient_utils.Error("client not configured; see 'gclient config'")
kbr@google.comab318592009-09-04 00:54:55 +0000974 if options.verbose:
975 # Print out the .gclient file. This is longer than if we just printed the
976 # client dict, but more legible, and it might contain helpful comments.
977 print(client.ConfigContent())
kbr@google.comab318592009-09-04 00:54:55 +0000978 return client.RunOnDeps('pack', args)
979
980
maruel@chromium.org79692d62010-05-14 18:57:13 +0000981def CMDstatus(options, args):
982 """Show the status of client and dependent modules, using 'svn diff'
983for each module. Additional options and args may be passed to 'svn diff'.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000984
maruel@chromium.org79692d62010-05-14 18:57:13 +0000985usage: status [options] [--] [svn diff args/options]
986
987Valid options:
988 --verbose : output additional diagnostics
989 --nohooks : don't run the hooks after the update is complete
990"""
maruel@chromium.org2806acc2009-05-15 12:33:34 +0000991 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000992 if not client:
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000993 raise gclient_utils.Error("client not configured; see 'gclient config'")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000994 if options.verbose:
995 # Print out the .gclient file. This is longer than if we just printed the
996 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000997 print(client.ConfigContent())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000998 return client.RunOnDeps('status', args)
999
1000
maruel@chromium.org79692d62010-05-14 18:57:13 +00001001def CMDsync(options, args):
1002 """Perform a checkout/update of the modules specified by the gclient
1003configuration; see 'help config'. Unless --revision is specified,
1004then the latest revision of the root solutions is checked out, with
1005dependent submodule versions updated according to DEPS files.
1006If --revision is specified, then the given revision is used in place
1007of the latest, either for a single solution or for all solutions.
1008Unless the --force option is provided, solutions and modules whose
1009local revision matches the one to update (i.e., they have not changed
1010in the repository) are *not* modified. Unless --nohooks is provided,
1011the hooks are run.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001012
maruel@chromium.org79692d62010-05-14 18:57:13 +00001013usage: gclient sync [options] [--] [SCM update options/args]
1014
1015Valid options:
1016 --force : force update even for unchanged modules
1017 --nohooks : don't run the hooks after the update is complete
1018 --revision SOLUTION@REV : update given solution to specified revision
1019 --deps PLATFORM(S) : sync deps for the given platform(s), or 'all'
1020 --verbose : output additional diagnostics
1021 --head : update to latest revision, instead of last good
1022 revision
1023 --reset : resets any local changes before updating (git only)
1024
1025Examples:
1026 gclient sync
1027 update files from SCM according to current configuration,
1028 *for modules which have changed since last update or sync*
1029 gclient sync --force
1030 update files from SCM according to current configuration, for
1031 all modules (useful for recovering files deleted from local copy)
1032 gclient sync --revision src@31000
1033 update src directory to r31000
1034"""
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001035 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001036
1037 if not client:
maruel@chromium.orge3608df2009-11-10 20:22:57 +00001038 raise gclient_utils.Error("client not configured; see 'gclient config'")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001039
1040 if not options.head:
1041 solutions = client.GetVar('solutions')
1042 if solutions:
1043 for s in solutions:
1044 if s.get('safesync_url', ''):
1045 # rip through revisions and make sure we're not over-riding
1046 # something that was explicitly passed
1047 has_key = False
1048 for r in options.revisions:
1049 if r.split('@')[0] == s['name']:
1050 has_key = True
1051 break
1052
1053 if not has_key:
1054 handle = urllib.urlopen(s['safesync_url'])
1055 rev = handle.read().strip()
1056 handle.close()
1057 if len(rev):
1058 options.revisions.append(s['name']+'@'+rev)
1059
1060 if options.verbose:
1061 # Print out the .gclient file. This is longer than if we just printed the
1062 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +00001063 print(client.ConfigContent())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001064 return client.RunOnDeps('update', args)
1065
1066
maruel@chromium.org79692d62010-05-14 18:57:13 +00001067def CMDupdate(options, args):
1068 """Alias for the sync command. Deprecated.
1069"""
1070 return CMDsync(options, args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001071
maruel@chromium.org79692d62010-05-14 18:57:13 +00001072
1073def CMDdiff(options, args):
1074 """Display the differences between two revisions of modules.
1075(Does 'svn diff' for each checked out module and dependences.)
1076Additional args and options to 'svn diff' can be passed after
1077gclient options.
1078
1079usage: diff [options] [--] [svn args/options]
1080
1081Valid options:
1082 --verbose : output additional diagnostics
1083
1084Examples:
1085 gclient diff
1086 simple 'svn diff' for configured client and dependences
1087 gclient diff -- -x -b
1088 use 'svn diff -x -b' to suppress whitespace-only differences
1089 gclient diff -- -r HEAD -x -b
1090 diff versus the latest version of each module
1091"""
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001092 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001093 if not client:
maruel@chromium.orge3608df2009-11-10 20:22:57 +00001094 raise gclient_utils.Error("client not configured; see 'gclient config'")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001095 if options.verbose:
1096 # Print out the .gclient file. This is longer than if we just printed the
1097 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +00001098 print(client.ConfigContent())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001099 return client.RunOnDeps('diff', args)
1100
1101
maruel@chromium.org79692d62010-05-14 18:57:13 +00001102def CMDrevert(options, args):
1103 """Revert every file in every managed directory in the client view.
1104"""
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001105 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001106 if not client:
maruel@chromium.orge3608df2009-11-10 20:22:57 +00001107 raise gclient_utils.Error("client not configured; see 'gclient config'")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001108 return client.RunOnDeps('revert', args)
1109
1110
maruel@chromium.org79692d62010-05-14 18:57:13 +00001111def CMDrunhooks(options, args):
1112 """Runs hooks for files that have been modified in the local working copy,
1113according to 'svn status'. Implies --force.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001114
maruel@chromium.org79692d62010-05-14 18:57:13 +00001115usage: runhooks [options]
1116
1117Valid options:
1118 --verbose : output additional diagnostics
1119"""
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001120 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001121 if not client:
maruel@chromium.orge3608df2009-11-10 20:22:57 +00001122 raise gclient_utils.Error("client not configured; see 'gclient config'")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001123 if options.verbose:
1124 # Print out the .gclient file. This is longer than if we just printed the
1125 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +00001126 print(client.ConfigContent())
maruel@chromium.org5df6a462009-08-28 18:52:26 +00001127 options.force = True
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001128 return client.RunOnDeps('runhooks', args)
1129
1130
maruel@chromium.org79692d62010-05-14 18:57:13 +00001131def CMDrevinfo(options, args):
1132 """Outputs source path, server URL and revision information for every
1133dependency in all solutions.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001134
maruel@chromium.org79692d62010-05-14 18:57:13 +00001135usage: revinfo [options]
1136"""
maruel@chromium.orge3608df2009-11-10 20:22:57 +00001137 __pychecker__ = 'unusednames=args'
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001138 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001139 if not client:
maruel@chromium.orge3608df2009-11-10 20:22:57 +00001140 raise gclient_utils.Error("client not configured; see 'gclient config'")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001141 client.PrintRevInfo()
maruel@chromium.org79692d62010-05-14 18:57:13 +00001142 return 0
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001143
1144
maruel@chromium.org79692d62010-05-14 18:57:13 +00001145def DispatchCommand(command, options, args):
1146 """Dispatches the appropriate subcommand based on command line arguments.
1147"""
1148 module = sys.modules[__name__]
1149 command = getattr(module, 'CMD' + command, None)
1150 if command:
1151 return command(options, args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001152 else:
maruel@chromium.orge3608df2009-11-10 20:22:57 +00001153 raise gclient_utils.Error("unknown subcommand '%s'; see 'gclient help'" %
1154 command)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001155
1156
1157def Main(argv):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001158 option_parser = optparse.OptionParser(usage=DEFAULT_USAGE_TEXT,
1159 version=__version__)
maruel@chromium.orge9e22232010-05-16 23:48:58 +00001160 option_parser.add_option("--force", action="store_true",
1161 help="(update/sync only) force update even "
1162 "for modules which haven't changed")
1163 option_parser.add_option("--nohooks", action="store_true",
1164 help="(update/sync/revert only) prevent the hooks "
1165 "from running")
1166 option_parser.add_option("--revision", action="append", dest="revisions",
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001167 metavar="REV", default=[],
maruel@chromium.orge9e22232010-05-16 23:48:58 +00001168 help="(update/sync only) sync to a specific "
1169 "revision, can be used multiple times for "
1170 "each solution, e.g. --revision=src@123, "
1171 "--revision=internal@32")
1172 option_parser.add_option("--deps", dest="deps_os", metavar="OS_LIST",
1173 help="(update/sync only) sync deps for the "
1174 "specified (comma-separated) platform(s); "
1175 "'all' will sync all platforms")
1176 option_parser.add_option("--reset", action="store_true",
1177 help="(update/sync only) resets any local changes "
1178 "before updating (git only)")
1179 option_parser.add_option("--spec",
1180 help="(config only) create a gclient file "
1181 "containing the provided string")
maruel@chromium.orga6220d12010-01-06 21:04:17 +00001182 option_parser.add_option("-v", "--verbose", action="count", default=0,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001183 help="produce additional output for diagnostics")
maruel@chromium.orge9e22232010-05-16 23:48:58 +00001184 option_parser.add_option("--manually_grab_svn_rev", action="store_true",
maruel@chromium.org7753d242009-10-07 17:40:24 +00001185 help="Skip svn up whenever possible by requesting "
1186 "actual HEAD revision from the repository")
maruel@chromium.orge9e22232010-05-16 23:48:58 +00001187 option_parser.add_option("--head", action="store_true",
1188 help="skips any safesync_urls specified in "
1189 "configured solutions")
1190 option_parser.add_option("--delete_unversioned_trees", action="store_true",
1191 help="on update, delete any unexpected "
1192 "unversioned trees that are in the checkout")
1193 option_parser.add_option("--snapshot", action="store_true",
1194 help="(revinfo only), create a snapshot file "
1195 "of the current version of all repositories")
1196 option_parser.add_option("--name",
iposva@chromium.org8cf7a392010-04-07 17:20:26 +00001197 help="specify alternate relative solution path")
maruel@chromium.orge9e22232010-05-16 23:48:58 +00001198 option_parser.add_option("--gclientfile", metavar="FILENAME",
1199 help="specify an alternate .gclient file")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001200
1201 if len(argv) < 2:
1202 # Users don't need to be told to use the 'help' command.
1203 option_parser.print_help()
1204 return 1
1205 # Add manual support for --version as first argument.
1206 if argv[1] == '--version':
1207 option_parser.print_version()
1208 return 0
1209
1210 # Add manual support for --help as first argument.
1211 if argv[1] == '--help':
1212 argv[1] = 'help'
1213
1214 command = argv[1]
1215 options, args = option_parser.parse_args(argv[2:])
1216
1217 if len(argv) < 3 and command == "help":
1218 option_parser.print_help()
1219 return 0
1220
maruel@chromium.orga6220d12010-01-06 21:04:17 +00001221 if options.verbose > 1:
maruel@chromium.org754960e2009-09-21 12:31:05 +00001222 logging.basicConfig(level=logging.DEBUG)
1223
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001224 # Files used for configuration and state saving.
1225 options.config_filename = os.environ.get("GCLIENT_FILE", ".gclient")
maruel@chromium.orge3da35f2010-03-09 21:40:45 +00001226 if options.gclientfile:
1227 options.config_filename = options.gclientfile
1228 options.entries_filename = options.config_filename + "_entries"
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001229 options.deps_file = "DEPS"
1230
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001231 options.platform = sys.platform
1232 return DispatchCommand(command, options, args)
1233
1234
1235if "__main__" == __name__:
1236 try:
1237 result = Main(sys.argv)
maruel@chromium.orge3608df2009-11-10 20:22:57 +00001238 except gclient_utils.Error, e:
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +00001239 print >> sys.stderr, "Error: %s" % str(e)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001240 result = 1
1241 sys.exit(result)
1242
1243# vim: ts=2:sw=2:tw=80:et: