blob: a72cc5c92287edd3dee845297955bca9e87bcf8b [file] [log] [blame]
[email protected]fb2b8eb2009-04-23 21:03:421#!/usr/bin/python
2#
3# Copyright 2008 Google Inc. All Rights Reserved.
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16
17"""A wrapper script to manage a set of client modules in different SCM.
18
19This script is intended to be used to help basic management of client
20program sources residing in one or more Subversion modules, along with
21other modules it depends on, also in Subversion, but possibly on
22multiple respositories, making a wrapper system apparently necessary.
23
24Files
25 .gclient : Current client configuration, written by 'config' command.
26 Format is a Python script defining 'solutions', a list whose
27 entries each are maps binding the strings "name" and "url"
28 to strings specifying the name and location of the client
29 module, as well as "custom_deps" to a map similar to the DEPS
30 file below.
31 .gclient_entries : A cache constructed by 'update' command. Format is a
32 Python script defining 'entries', a list of the names
33 of all modules in the client
34 <module>/DEPS : Python script defining var 'deps' as a map from each requisite
35 submodule name to a URL where it can be found (via one SCM)
36
37Hooks
38 .gclient and DEPS files may optionally contain a list named "hooks" to
39 allow custom actions to be performed based on files that have changed in the
[email protected]67820ef2009-07-27 17:23:0040 working copy as a result of a "sync"/"update" or "revert" operation. This
41 could be prevented by using --nohooks (hooks run by default). Hooks can also
42 be run based on what files have been modified in the working copy
[email protected]fb2b8eb2009-04-23 21:03:4243 with the "runhooks" operation. If any of these operation are run with
44 --force, all known hooks will run regardless of the state of the working
45 copy.
46
47 Each item in a "hooks" list is a dict, containing these two keys:
48 "pattern" The associated value is a string containing a regular
49 expression. When a file whose pathname matches the expression
50 is checked out, updated, or reverted, the hook's "action" will
51 run.
52 "action" A list describing a command to run along with its arguments, if
53 any. An action command will run at most one time per gclient
54 invocation, regardless of how many files matched the pattern.
55 The action is executed in the same directory as the .gclient
56 file. If the first item in the list is the string "python",
57 the current Python interpreter (sys.executable) will be used
[email protected]71b40682009-07-31 23:40:0958 to run the command. If the list contains string "$matching_files"
59 it will be removed from the list and the list will be extended
60 by the list of matching files.
[email protected]fb2b8eb2009-04-23 21:03:4261
62 Example:
63 hooks = [
64 { "pattern": "\\.(gif|jpe?g|pr0n|png)$",
65 "action": ["python", "image_indexer.py", "--all"]},
66 ]
67"""
68
69__author__ = "[email protected] (Darin Fisher)"
[email protected]df7a3132009-05-12 17:49:4970__version__ = "0.3.2"
[email protected]fb2b8eb2009-04-23 21:03:4271
72import errno
73import optparse
74import os
75import re
76import stat
77import subprocess
78import sys
79import time
80import urlparse
81import xml.dom.minidom
82import urllib
83
[email protected]fb2b8eb2009-04-23 21:03:4284
85SVN_COMMAND = "svn"
86
87
88# default help text
89DEFAULT_USAGE_TEXT = (
90"""usage: %prog <subcommand> [options] [--] [svn options/args...]
91a wrapper for managing a set of client modules in svn.
92Version """ + __version__ + """
93
94subcommands:
95 cleanup
96 config
97 diff
[email protected]644aa0c2009-07-17 20:20:4198 export
[email protected]fb2b8eb2009-04-23 21:03:4299 revert
100 status
101 sync
102 update
103 runhooks
104 revinfo
105
106Options and extra arguments can be passed to invoked svn commands by
107appending them to the command line. Note that if the first such
108appended option starts with a dash (-) then the options must be
109preceded by -- to distinguish them from gclient options.
110
111For additional help on a subcommand or examples of usage, try
112 %prog help <subcommand>
113 %prog help files
114""")
115
116GENERIC_UPDATE_USAGE_TEXT = (
117 """Perform a checkout/update of the modules specified by the gclient
118configuration; see 'help config'. Unless --revision is specified,
119then the latest revision of the root solutions is checked out, with
120dependent submodule versions updated according to DEPS files.
121If --revision is specified, then the given revision is used in place
122of the latest, either for a single solution or for all solutions.
123Unless the --force option is provided, solutions and modules whose
124local revision matches the one to update (i.e., they have not changed
[email protected]67820ef2009-07-27 17:23:00125in the repository) are *not* modified. Unless --nohooks is provided,
126the hooks are run.
[email protected]fb2b8eb2009-04-23 21:03:42127This a synonym for 'gclient %(alias)s'
128
129usage: gclient %(cmd)s [options] [--] [svn update options/args]
130
131Valid options:
132 --force : force update even for unchanged modules
[email protected]67820ef2009-07-27 17:23:00133 --nohooks : don't run the hooks after the update is complete
[email protected]fb2b8eb2009-04-23 21:03:42134 --revision REV : update/checkout all solutions with specified revision
135 --revision SOLUTION@REV : update given solution to specified revision
136 --deps PLATFORM(S) : sync deps for the given platform(s), or 'all'
137 --verbose : output additional diagnostics
[email protected]b8b6b872009-06-30 18:50:56138 --head : update to latest revision, instead of last good revision
[email protected]fb2b8eb2009-04-23 21:03:42139
140Examples:
141 gclient %(cmd)s
142 update files from SVN according to current configuration,
143 *for modules which have changed since last update or sync*
144 gclient %(cmd)s --force
145 update files from SVN according to current configuration, for
146 all modules (useful for recovering files deleted from local copy)
147""")
148
149COMMAND_USAGE_TEXT = {
150 "cleanup":
151 """Clean up all working copies, using 'svn cleanup' for each module.
152Additional options and args may be passed to 'svn cleanup'.
153
154usage: cleanup [options] [--] [svn cleanup args/options]
155
156Valid options:
157 --verbose : output additional diagnostics
158""",
159 "config": """Create a .gclient file in the current directory; this
160specifies the configuration for further commands. After update/sync,
161top-level DEPS files in each module are read to determine dependent
162modules to operate on as well. If optional [url] parameter is
163provided, then configuration is read from a specified Subversion server
164URL. Otherwise, a --spec option must be provided.
165
166usage: config [option | url] [safesync url]
167
168Valid options:
169 --spec=GCLIENT_SPEC : contents of .gclient are read from string parameter.
170 *Note that due to Cygwin/Python brokenness, it
171 probably can't contain any newlines.*
172
173Examples:
174 gclient config https://gclient.googlecode.com/svn/trunk/gclient
175 configure a new client to check out gclient.py tool sources
176 gclient config --spec='solutions=[{"name":"gclient","""
177 '"url":"https://gclient.googlecode.com/svn/trunk/gclient",'
178 '"custom_deps":{}}]',
179 "diff": """Display the differences between two revisions of modules.
180(Does 'svn diff' for each checked out module and dependences.)
181Additional args and options to 'svn diff' can be passed after
182gclient options.
183
184usage: diff [options] [--] [svn args/options]
185
186Valid options:
187 --verbose : output additional diagnostics
188
189Examples:
190 gclient diff
191 simple 'svn diff' for configured client and dependences
192 gclient diff -- -x -b
193 use 'svn diff -x -b' to suppress whitespace-only differences
194 gclient diff -- -r HEAD -x -b
195 diff versus the latest version of each module
196""",
[email protected]644aa0c2009-07-17 20:20:41197 "export":
198 """Wrapper for svn export for all managed directories
199""",
[email protected]fb2b8eb2009-04-23 21:03:42200 "revert":
201 """Revert every file in every managed directory in the client view.
202
203usage: revert
204""",
205 "status":
206 """Show the status of client and dependent modules, using 'svn diff'
207for each module. Additional options and args may be passed to 'svn diff'.
208
209usage: status [options] [--] [svn diff args/options]
210
211Valid options:
212 --verbose : output additional diagnostics
[email protected]67820ef2009-07-27 17:23:00213 --nohooks : don't run the hooks after the update is complete
[email protected]fb2b8eb2009-04-23 21:03:42214""",
215 "sync": GENERIC_UPDATE_USAGE_TEXT % {"cmd": "sync", "alias": "update"},
216 "update": GENERIC_UPDATE_USAGE_TEXT % {"cmd": "update", "alias": "sync"},
217 "help": """Describe the usage of this program or its subcommands.
218
219usage: help [options] [subcommand]
220
221Valid options:
222 --verbose : output additional diagnostics
223""",
224 "runhooks":
225 """Runs hooks for files that have been modified in the local working copy,
226according to 'svn status'.
227
228usage: runhooks [options]
229
230Valid options:
231 --force : runs all known hooks, regardless of the working
232 copy status
233 --verbose : output additional diagnostics
234""",
235 "revinfo":
236 """Outputs source path, server URL and revision information for every
237dependency in all solutions (no local checkout required).
238
239usage: revinfo [options]
240""",
241}
242
243# parameterized by (solution_name, solution_url, safesync_url)
244DEFAULT_CLIENT_FILE_TEXT = (
245 """
246# An element of this array (a \"solution\") describes a repository directory
247# that will be checked out into your working copy. Each solution may
248# optionally define additional dependencies (via its DEPS file) to be
249# checked out alongside the solution's directory. A solution may also
250# specify custom dependencies (via the \"custom_deps\" property) that
251# override or augment the dependencies specified by the DEPS file.
252# If a \"safesync_url\" is specified, it is assumed to reference the location of
253# a text file which contains nothing but the last known good SCM revision to
254# sync against. It is fetched if specified and used unless --head is passed
255solutions = [
256 { \"name\" : \"%s\",
257 \"url\" : \"%s\",
258 \"custom_deps\" : {
259 # To use the trunk of a component instead of what's in DEPS:
260 #\"component\": \"https://svnserver/component/trunk/\",
261 # To exclude a component from your working copy:
262 #\"data/really_large_component\": None,
263 },
264 \"safesync_url\": \"%s\"
265 }
266]
267""")
268
269
270## Generic utils
271
[email protected]e105d8d2009-04-30 17:58:25272def ParseXML(output):
273 try:
274 return xml.dom.minidom.parseString(output)
275 except xml.parsers.expat.ExpatError:
276 return None
277
278
[email protected]483b0082009-05-07 02:57:14279def GetNamedNodeText(node, node_name):
280 child_nodes = node.getElementsByTagName(node_name)
281 if not child_nodes:
282 return None
283 assert len(child_nodes) == 1 and child_nodes[0].childNodes.length == 1
284 return child_nodes[0].firstChild.nodeValue
285
286
287def GetNodeNamedAttributeText(node, node_name, attribute_name):
288 child_nodes = node.getElementsByTagName(node_name)
289 if not child_nodes:
290 return None
291 assert len(child_nodes) == 1
292 return child_nodes[0].getAttribute(attribute_name)
293
294
[email protected]fb2b8eb2009-04-23 21:03:42295class Error(Exception):
296 """gclient exception class."""
297 pass
298
299class PrintableObject(object):
300 def __str__(self):
301 output = ''
302 for i in dir(self):
303 if i.startswith('__'):
304 continue
305 output += '%s = %s\n' % (i, str(getattr(self, i, '')))
306 return output
307
308
309def FileRead(filename):
310 content = None
311 f = open(filename, "rU")
312 try:
313 content = f.read()
314 finally:
315 f.close()
316 return content
317
318
319def FileWrite(filename, content):
320 f = open(filename, "w")
321 try:
322 f.write(content)
323 finally:
324 f.close()
325
326
327def RemoveDirectory(*path):
328 """Recursively removes a directory, even if it's marked read-only.
329
330 Remove the directory located at *path, if it exists.
331
332 shutil.rmtree() doesn't work on Windows if any of the files or directories
333 are read-only, which svn repositories and some .svn files are. We need to
334 be able to force the files to be writable (i.e., deletable) as we traverse
335 the tree.
336
337 Even with all this, Windows still sometimes fails to delete a file, citing
338 a permission error (maybe something to do with antivirus scans or disk
339 indexing). The best suggestion any of the user forums had was to wait a
340 bit and try again, so we do that too. It's hand-waving, but sometimes it
341 works. :/
342
343 On POSIX systems, things are a little bit simpler. The modes of the files
344 to be deleted doesn't matter, only the modes of the directories containing
345 them are significant. As the directory tree is traversed, each directory
346 has its mode set appropriately before descending into it. This should
347 result in the entire tree being removed, with the possible exception of
348 *path itself, because nothing attempts to change the mode of its parent.
349 Doing so would be hazardous, as it's not a directory slated for removal.
350 In the ordinary case, this is not a problem: for our purposes, the user
351 will never lack write permission on *path's parent.
352 """
353 file_path = os.path.join(*path)
354 if not os.path.exists(file_path):
355 return
356
357 if os.path.islink(file_path) or not os.path.isdir(file_path):
358 raise Error("RemoveDirectory asked to remove non-directory %s" % file_path)
359
360 has_win32api = False
361 if sys.platform == 'win32':
362 has_win32api = True
363 # Some people don't have the APIs installed. In that case we'll do without.
364 try:
365 win32api = __import__('win32api')
366 win32con = __import__('win32con')
367 except ImportError:
368 has_win32api = False
369 else:
370 # On POSIX systems, we need the x-bit set on the directory to access it,
371 # the r-bit to see its contents, and the w-bit to remove files from it.
372 # The actual modes of the files within the directory is irrelevant.
373 os.chmod(file_path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
374 for fn in os.listdir(file_path):
375 fullpath = os.path.join(file_path, fn)
376
377 # If fullpath is a symbolic link that points to a directory, isdir will
378 # be True, but we don't want to descend into that as a directory, we just
379 # want to remove the link. Check islink and treat links as ordinary files
380 # would be treated regardless of what they reference.
381 if os.path.islink(fullpath) or not os.path.isdir(fullpath):
382 if sys.platform == 'win32':
383 os.chmod(fullpath, stat.S_IWRITE)
384 if has_win32api:
385 win32api.SetFileAttributes(fullpath, win32con.FILE_ATTRIBUTE_NORMAL)
386 try:
387 os.remove(fullpath)
388 except OSError, e:
389 if e.errno != errno.EACCES or sys.platform != 'win32':
390 raise
391 print 'Failed to delete %s: trying again' % fullpath
392 time.sleep(0.1)
393 os.remove(fullpath)
394 else:
395 RemoveDirectory(fullpath)
396
397 if sys.platform == 'win32':
398 os.chmod(file_path, stat.S_IWRITE)
399 if has_win32api:
400 win32api.SetFileAttributes(file_path, win32con.FILE_ATTRIBUTE_NORMAL)
401 try:
402 os.rmdir(file_path)
403 except OSError, e:
404 if e.errno != errno.EACCES or sys.platform != 'win32':
405 raise
406 print 'Failed to remove %s: trying again' % file_path
407 time.sleep(0.1)
408 os.rmdir(file_path)
409
410
[email protected]df7a3132009-05-12 17:49:49411def SubprocessCall(command, in_directory, fail_status=None):
[email protected]fb2b8eb2009-04-23 21:03:42412 """Runs command, a list, in directory in_directory.
413
414 This function wraps SubprocessCallAndCapture, but does not perform the
415 capturing functions. See that function for a more complete usage
416 description.
417 """
418 # Call subprocess and capture nothing:
[email protected]df7a3132009-05-12 17:49:49419 SubprocessCallAndCapture(command, in_directory, fail_status)
[email protected]fb2b8eb2009-04-23 21:03:42420
421
[email protected]df7a3132009-05-12 17:49:49422def SubprocessCallAndCapture(command, in_directory, fail_status=None,
[email protected]fb2b8eb2009-04-23 21:03:42423 pattern=None, capture_list=None):
424 """Runs command, a list, in directory in_directory.
425
426 A message indicating what is being done, as well as the command's stdout,
427 is printed to out.
428
429 If a pattern is specified, any line in the output matching pattern will have
430 its first match group appended to capture_list.
431
432 If the command fails, as indicated by a nonzero exit status, gclient will
433 exit with an exit status of fail_status. If fail_status is None (the
434 default), gclient will raise an Error exception.
435 """
436
[email protected]df7a3132009-05-12 17:49:49437 print("\n________ running \'%s\' in \'%s\'"
438 % (' '.join(command), in_directory))
[email protected]fb2b8eb2009-04-23 21:03:42439
440 # *Sigh*: Windows needs shell=True, or else it won't search %PATH% for the
441 # executable, but shell=True makes subprocess on Linux fail when it's called
442 # with a list because it only tries to execute the first item in the list.
443 kid = subprocess.Popen(command, bufsize=0, cwd=in_directory,
444 shell=(sys.platform == 'win32'), stdout=subprocess.PIPE)
445
446 if pattern:
447 compiled_pattern = re.compile(pattern)
448
449 # Also, we need to forward stdout to prevent weird re-ordering of output.
450 # This has to be done on a per byte basis to make sure it is not buffered:
451 # normally buffering is done for each line, but if svn requests input, no
452 # end-of-line character is output after the prompt and it would not show up.
453 in_byte = kid.stdout.read(1)
454 in_line = ""
455 while in_byte:
456 if in_byte != "\r":
[email protected]df7a3132009-05-12 17:49:49457 sys.stdout.write(in_byte)
[email protected]fb2b8eb2009-04-23 21:03:42458 in_line += in_byte
459 if in_byte == "\n" and pattern:
460 match = compiled_pattern.search(in_line[:-1])
461 if match:
462 capture_list.append(match.group(1))
463 in_line = ""
464 in_byte = kid.stdout.read(1)
465 rv = kid.wait()
466
467 if rv:
468 msg = "failed to run command: %s" % " ".join(command)
469
470 if fail_status != None:
471 print >>sys.stderr, msg
472 sys.exit(fail_status)
473
474 raise Error(msg)
475
476
477def IsUsingGit(root, paths):
478 """Returns True if we're using git to manage any of our checkouts.
479 |entries| is a list of paths to check."""
480 for path in paths:
481 if os.path.exists(os.path.join(root, path, '.git')):
482 return True
483 return False
484
485# -----------------------------------------------------------------------------
486# SVN utils:
487
488
[email protected]df7a3132009-05-12 17:49:49489def RunSVN(args, in_directory):
[email protected]fb2b8eb2009-04-23 21:03:42490 """Runs svn, sending output to stdout.
491
492 Args:
493 args: A sequence of command line parameters to be passed to svn.
494 in_directory: The directory where svn is to be run.
495
496 Raises:
497 Error: An error occurred while running the svn command.
498 """
499 c = [SVN_COMMAND]
500 c.extend(args)
501
[email protected]df7a3132009-05-12 17:49:49502 SubprocessCall(c, in_directory)
[email protected]fb2b8eb2009-04-23 21:03:42503
504
[email protected]5c3a2ff2009-05-12 19:28:55505def CaptureSVN(args, in_directory=None, print_error=True):
[email protected]fb2b8eb2009-04-23 21:03:42506 """Runs svn, capturing output sent to stdout as a string.
507
508 Args:
509 args: A sequence of command line parameters to be passed to svn.
510 in_directory: The directory where svn is to be run.
511
512 Returns:
513 The output sent to stdout as a string.
514 """
515 c = [SVN_COMMAND]
516 c.extend(args)
517
518 # *Sigh*: Windows needs shell=True, or else it won't search %PATH% for
519 # the svn.exe executable, but shell=True makes subprocess on Linux fail
520 # when it's called with a list because it only tries to execute the
521 # first string ("svn").
[email protected]5c3a2ff2009-05-12 19:28:55522 stderr = None
[email protected]672343d2009-05-20 20:03:25523 if not print_error:
[email protected]5c3a2ff2009-05-12 19:28:55524 stderr = subprocess.PIPE
[email protected]df7a3132009-05-12 17:49:49525 return subprocess.Popen(c,
526 cwd=in_directory,
527 shell=(sys.platform == 'win32'),
[email protected]5c3a2ff2009-05-12 19:28:55528 stdout=subprocess.PIPE,
529 stderr=stderr).communicate()[0]
[email protected]fb2b8eb2009-04-23 21:03:42530
531
[email protected]df7a3132009-05-12 17:49:49532def RunSVNAndGetFileList(args, in_directory, file_list):
[email protected]fb2b8eb2009-04-23 21:03:42533 """Runs svn checkout, update, or status, output to stdout.
534
535 The first item in args must be either "checkout", "update", or "status".
536
537 svn's stdout is parsed to collect a list of files checked out or updated.
538 These files are appended to file_list. svn's stdout is also printed to
539 sys.stdout as in RunSVN.
540
541 Args:
542 args: A sequence of command line parameters to be passed to svn.
543 in_directory: The directory where svn is to be run.
544
545 Raises:
546 Error: An error occurred while running the svn command.
547 """
548 command = [SVN_COMMAND]
549 command.extend(args)
550
551 # svn update and svn checkout use the same pattern: the first three columns
552 # are for file status, property status, and lock status. This is followed
553 # by two spaces, and then the path to the file.
554 update_pattern = '^... (.*)$'
555
556 # The first three columns of svn status are the same as for svn update and
557 # svn checkout. The next three columns indicate addition-with-history,
558 # switch, and remote lock status. This is followed by one space, and then
559 # the path to the file.
560 status_pattern = '^...... (.*)$'
561
562 # args[0] must be a supported command. This will blow up if it's something
563 # else, which is good. Note that the patterns are only effective when
564 # these commands are used in their ordinary forms, the patterns are invalid
565 # for "svn status --show-updates", for example.
566 pattern = {
567 'checkout': update_pattern,
568 'status': status_pattern,
569 'update': update_pattern,
570 }[args[0]]
571
[email protected]df7a3132009-05-12 17:49:49572 SubprocessCallAndCapture(command,
573 in_directory,
574 pattern=pattern,
575 capture_list=file_list)
[email protected]fb2b8eb2009-04-23 21:03:42576
577
[email protected]5c3a2ff2009-05-12 19:28:55578def CaptureSVNInfo(relpath, in_directory=None, print_error=True):
[email protected]2dc8a4d2009-05-11 12:41:20579 """Returns a dictionary from the svn info output for the given file.
[email protected]fb2b8eb2009-04-23 21:03:42580
581 Args:
582 relpath: The directory where the working copy resides relative to
583 the directory given by in_directory.
584 in_directory: The directory where svn is to be run.
[email protected]fb2b8eb2009-04-23 21:03:42585 """
[email protected]5c3a2ff2009-05-12 19:28:55586 output = CaptureSVN(["info", "--xml", relpath], in_directory, print_error)
[email protected]df7a3132009-05-12 17:49:49587 dom = ParseXML(output)
[email protected]2dc8a4d2009-05-11 12:41:20588 result = {}
[email protected]483b0082009-05-07 02:57:14589 if dom:
[email protected]2dc8a4d2009-05-11 12:41:20590 def C(item, f):
591 if item is not None: return f(item)
[email protected]483b0082009-05-07 02:57:14592 # /info/entry/
593 # url
594 # reposityory/(root|uuid)
595 # wc-info/(schedule|depth)
596 # commit/(author|date)
597 # str() the results because they may be returned as Unicode, which
598 # interferes with the higher layers matching up things in the deps
599 # dictionary.
[email protected]2dc8a4d2009-05-11 12:41:20600 # TODO(maruel): Fix at higher level instead (!)
601 result['Repository Root'] = C(GetNamedNodeText(dom, 'root'), str)
602 result['URL'] = C(GetNamedNodeText(dom, 'url'), str)
603 result['UUID'] = C(GetNamedNodeText(dom, 'uuid'), str)
604 result['Revision'] = C(GetNodeNamedAttributeText(dom, 'entry', 'revision'),
605 int)
606 result['Node Kind'] = C(GetNodeNamedAttributeText(dom, 'entry', 'kind'),
607 str)
608 result['Schedule'] = C(GetNamedNodeText(dom, 'schedule'), str)
609 result['Path'] = C(GetNodeNamedAttributeText(dom, 'entry', 'path'), str)
610 result['Copied From URL'] = C(GetNamedNodeText(dom, 'copy-from-url'), str)
611 result['Copied From Rev'] = C(GetNamedNodeText(dom, 'copy-from-rev'), str)
[email protected]fb2b8eb2009-04-23 21:03:42612 return result
613
614
[email protected]df7a3132009-05-12 17:49:49615def CaptureSVNHeadRevision(url):
[email protected]fb2b8eb2009-04-23 21:03:42616 """Get the head revision of a SVN repository.
617
618 Returns:
619 Int head revision
620 """
[email protected]df7a3132009-05-12 17:49:49621 info = CaptureSVN(["info", "--xml", url], os.getcwd())
[email protected]fb2b8eb2009-04-23 21:03:42622 dom = xml.dom.minidom.parseString(info)
623 return int(dom.getElementsByTagName('entry')[0].getAttribute('revision'))
624
625
[email protected]4810a962009-05-12 21:03:34626def CaptureSVNStatus(files):
627 """Returns the svn 1.5 svn status emulated output.
[email protected]fb2b8eb2009-04-23 21:03:42628
[email protected]4810a962009-05-12 21:03:34629 @files can be a string (one file) or a list of files.
[email protected]fb2b8eb2009-04-23 21:03:42630
[email protected]4810a962009-05-12 21:03:34631 Returns an array of (status, file) tuples."""
632 command = ["status", "--xml"]
633 if not files:
634 pass
635 elif isinstance(files, basestring):
636 command.append(files)
637 else:
638 command.extend(files)
[email protected]fb2b8eb2009-04-23 21:03:42639
[email protected]4810a962009-05-12 21:03:34640 status_letter = {
641 None: ' ',
642 '': ' ',
643 'added': 'A',
644 'conflicted': 'C',
645 'deleted': 'D',
646 'external': 'X',
647 'ignored': 'I',
648 'incomplete': '!',
649 'merged': 'G',
650 'missing': '!',
651 'modified': 'M',
652 'none': ' ',
653 'normal': ' ',
654 'obstructed': '~',
655 'replaced': 'R',
656 'unversioned': '?',
657 }
658 dom = ParseXML(CaptureSVN(command))
[email protected]e105d8d2009-04-30 17:58:25659 results = []
660 if dom:
661 # /status/target/entry/(wc-status|commit|author|date)
662 for target in dom.getElementsByTagName('target'):
663 base_path = target.getAttribute('path')
664 for entry in target.getElementsByTagName('entry'):
665 file = entry.getAttribute('path')
666 wc_status = entry.getElementsByTagName('wc-status')
667 assert len(wc_status) == 1
668 # Emulate svn 1.5 status ouput...
669 statuses = [' ' for i in range(7)]
670 # Col 0
671 xml_item_status = wc_status[0].getAttribute('item')
[email protected]4810a962009-05-12 21:03:34672 if xml_item_status in status_letter:
673 statuses[0] = status_letter[xml_item_status]
[email protected]e105d8d2009-04-30 17:58:25674 else:
675 raise Exception('Unknown item status "%s"; please implement me!' %
676 xml_item_status)
677 # Col 1
678 xml_props_status = wc_status[0].getAttribute('props')
679 if xml_props_status == 'modified':
680 statuses[1] = 'M'
681 elif xml_props_status == 'conflicted':
682 statuses[1] = 'C'
683 elif (not xml_props_status or xml_props_status == 'none' or
684 xml_props_status == 'normal'):
685 pass
686 else:
687 raise Exception('Unknown props status "%s"; please implement me!' %
688 xml_props_status)
[email protected]edd27d12009-05-01 17:46:56689 # Col 2
690 if wc_status[0].getAttribute('wc-locked') == 'true':
691 statuses[2] = 'L'
[email protected]e105d8d2009-04-30 17:58:25692 # Col 3
693 if wc_status[0].getAttribute('copied') == 'true':
694 statuses[3] = '+'
[email protected]4810a962009-05-12 21:03:34695 item = (''.join(statuses), file)
[email protected]e105d8d2009-04-30 17:58:25696 results.append(item)
697 return results
[email protected]fb2b8eb2009-04-23 21:03:42698
699
700### SCM abstraction layer
701
702
703class SCMWrapper(object):
704 """Add necessary glue between all the supported SCM.
705
706 This is the abstraction layer to bind to different SCM. Since currently only
707 subversion is supported, a lot of subersionism remains. This can be sorted out
708 once another SCM is supported."""
709 def __init__(self, url=None, root_dir=None, relpath=None,
710 scm_name='svn'):
711 # TODO(maruel): Deduce the SCM from the url.
712 self.scm_name = scm_name
713 self.url = url
714 self._root_dir = root_dir
715 if self._root_dir:
[email protected]e105d8d2009-04-30 17:58:25716 self._root_dir = self._root_dir.replace('/', os.sep)
[email protected]fb2b8eb2009-04-23 21:03:42717 self.relpath = relpath
718 if self.relpath:
[email protected]e105d8d2009-04-30 17:58:25719 self.relpath = self.relpath.replace('/', os.sep)
[email protected]fb2b8eb2009-04-23 21:03:42720
721 def FullUrlForRelativeUrl(self, url):
722 # Find the forth '/' and strip from there. A bit hackish.
723 return '/'.join(self.url.split('/')[:4]) + url
724
725 def RunCommand(self, command, options, args, file_list=None):
726 # file_list will have all files that are modified appended to it.
727
728 if file_list == None:
729 file_list = []
730
731 commands = {
732 'cleanup': self.cleanup,
[email protected]644aa0c2009-07-17 20:20:41733 'export': self.export,
[email protected]fb2b8eb2009-04-23 21:03:42734 'update': self.update,
735 'revert': self.revert,
736 'status': self.status,
737 'diff': self.diff,
738 'runhooks': self.status,
739 }
740
741 if not command in commands:
742 raise Error('Unknown command %s' % command)
743
744 return commands[command](options, args, file_list)
745
746 def cleanup(self, options, args, file_list):
747 """Cleanup working copy."""
748 command = ['cleanup']
749 command.extend(args)
[email protected]df7a3132009-05-12 17:49:49750 RunSVN(command, os.path.join(self._root_dir, self.relpath))
[email protected]fb2b8eb2009-04-23 21:03:42751
752 def diff(self, options, args, file_list):
753 # NOTE: This function does not currently modify file_list.
754 command = ['diff']
755 command.extend(args)
[email protected]df7a3132009-05-12 17:49:49756 RunSVN(command, os.path.join(self._root_dir, self.relpath))
[email protected]fb2b8eb2009-04-23 21:03:42757
[email protected]644aa0c2009-07-17 20:20:41758 def export(self, options, args, file_list):
759 assert len(args) == 1
760 export_path = os.path.abspath(os.path.join(args[0], self.relpath))
761 try:
762 os.makedirs(export_path)
763 except OSError:
764 pass
765 assert os.path.exists(export_path)
766 command = ['export', '--force', '.']
767 command.append(export_path)
768 RunSVN(command, os.path.join(self._root_dir, self.relpath))
769
[email protected]fb2b8eb2009-04-23 21:03:42770 def update(self, options, args, file_list):
771 """Runs SCM to update or transparently checkout the working copy.
772
773 All updated files will be appended to file_list.
774
775 Raises:
776 Error: if can't get URL for relative path.
777 """
778 # Only update if git is not controlling the directory.
[email protected]8626ff72009-05-13 02:57:02779 checkout_path = os.path.join(self._root_dir, self.relpath)
[email protected]0329e672009-05-13 18:41:04780 git_path = os.path.join(self._root_dir, self.relpath, '.git')
781 if os.path.exists(git_path):
[email protected]df7a3132009-05-12 17:49:49782 print("________ found .git directory; skipping %s" % self.relpath)
[email protected]fb2b8eb2009-04-23 21:03:42783 return
784
785 if args:
786 raise Error("Unsupported argument(s): %s" % ",".join(args))
787
788 url = self.url
789 components = url.split("@")
790 revision = None
791 forced_revision = False
792 if options.revision:
793 # Override the revision number.
794 url = '%s@%s' % (components[0], str(options.revision))
795 revision = int(options.revision)
796 forced_revision = True
797 elif len(components) == 2:
798 revision = int(components[1])
799 forced_revision = True
800
801 rev_str = ""
802 if revision:
803 rev_str = ' at %d' % revision
804
[email protected]0329e672009-05-13 18:41:04805 if not os.path.exists(checkout_path):
[email protected]fb2b8eb2009-04-23 21:03:42806 # We need to checkout.
[email protected]8626ff72009-05-13 02:57:02807 command = ['checkout', url, checkout_path]
[email protected]edd27d12009-05-01 17:46:56808 if revision:
809 command.extend(['--revision', str(revision)])
[email protected]df7a3132009-05-12 17:49:49810 RunSVNAndGetFileList(command, self._root_dir, file_list)
[email protected]edd27d12009-05-01 17:46:56811 return
[email protected]fb2b8eb2009-04-23 21:03:42812
813 # Get the existing scm url and the revision number of the current checkout.
[email protected]8626ff72009-05-13 02:57:02814 from_info = CaptureSVNInfo(os.path.join(checkout_path, '.'), '.')
[email protected]1998c6d2009-05-15 12:38:12815 if not from_info:
816 raise Error("Can't update/checkout %r if an unversioned directory is "
817 "present. Delete the directory and try again." %
818 checkout_path)
[email protected]fb2b8eb2009-04-23 21:03:42819
820 if options.manually_grab_svn_rev:
821 # Retrieve the current HEAD version because svn is slow at null updates.
822 if not revision:
[email protected]df7a3132009-05-12 17:49:49823 from_info_live = CaptureSVNInfo(from_info['URL'], '.')
[email protected]2dc8a4d2009-05-11 12:41:20824 revision = int(from_info_live['Revision'])
[email protected]fb2b8eb2009-04-23 21:03:42825 rev_str = ' at %d' % revision
826
[email protected]2dc8a4d2009-05-11 12:41:20827 if from_info['URL'] != components[0]:
[email protected]df7a3132009-05-12 17:49:49828 to_info = CaptureSVNInfo(url, '.')
[email protected]8626ff72009-05-13 02:57:02829 can_switch = ((from_info['Repository Root'] != to_info['Repository Root'])
830 and (from_info['UUID'] == to_info['UUID']))
831 if can_switch:
832 print("\n_____ relocating %s to a new checkout" % self.relpath)
[email protected]fb2b8eb2009-04-23 21:03:42833 # We have different roots, so check if we can switch --relocate.
834 # Subversion only permits this if the repository UUIDs match.
[email protected]fb2b8eb2009-04-23 21:03:42835 # Perform the switch --relocate, then rewrite the from_url
836 # to reflect where we "are now." (This is the same way that
837 # Subversion itself handles the metadata when switch --relocate
838 # is used.) This makes the checks below for whether we
839 # can update to a revision or have to switch to a different
840 # branch work as expected.
841 # TODO(maruel): TEST ME !
[email protected]2dc8a4d2009-05-11 12:41:20842 command = ["switch", "--relocate",
843 from_info['Repository Root'],
844 to_info['Repository Root'],
[email protected]fb2b8eb2009-04-23 21:03:42845 self.relpath]
[email protected]df7a3132009-05-12 17:49:49846 RunSVN(command, self._root_dir)
[email protected]2dc8a4d2009-05-11 12:41:20847 from_info['URL'] = from_info['URL'].replace(
848 from_info['Repository Root'],
849 to_info['Repository Root'])
[email protected]8626ff72009-05-13 02:57:02850 else:
851 if CaptureSVNStatus(checkout_path):
852 raise Error("Can't switch the checkout to %s; UUID don't match and "
853 "there is local changes in %s. Delete the directory and "
854 "try again." % (url, checkout_path))
855 # Ok delete it.
856 print("\n_____ switching %s to a new checkout" % self.relpath)
857 RemoveDirectory(checkout_path)
858 # We need to checkout.
859 command = ['checkout', url, checkout_path]
860 if revision:
861 command.extend(['--revision', str(revision)])
862 RunSVNAndGetFileList(command, self._root_dir, file_list)
863 return
[email protected]71b40682009-07-31 23:40:09864
[email protected]fb2b8eb2009-04-23 21:03:42865
866 # If the provided url has a revision number that matches the revision
867 # number of the existing directory, then we don't need to bother updating.
[email protected]2dc8a4d2009-05-11 12:41:20868 if not options.force and from_info['Revision'] == revision:
[email protected]fb2b8eb2009-04-23 21:03:42869 if options.verbose or not forced_revision:
[email protected]df7a3132009-05-12 17:49:49870 print("\n_____ %s%s" % (self.relpath, rev_str))
[email protected]fb2b8eb2009-04-23 21:03:42871 return
872
[email protected]8626ff72009-05-13 02:57:02873 command = ["update", checkout_path]
[email protected]fb2b8eb2009-04-23 21:03:42874 if revision:
875 command.extend(['--revision', str(revision)])
[email protected]df7a3132009-05-12 17:49:49876 RunSVNAndGetFileList(command, self._root_dir, file_list)
[email protected]fb2b8eb2009-04-23 21:03:42877
878 def revert(self, options, args, file_list):
879 """Reverts local modifications. Subversion specific.
880
881 All reverted files will be appended to file_list, even if Subversion
882 doesn't know about them.
883 """
884 path = os.path.join(self._root_dir, self.relpath)
885 if not os.path.isdir(path):
[email protected]edd27d12009-05-01 17:46:56886 # svn revert won't work if the directory doesn't exist. It needs to
887 # checkout instead.
[email protected]df7a3132009-05-12 17:49:49888 print("\n_____ %s is missing, synching instead" % self.relpath)
[email protected]edd27d12009-05-01 17:46:56889 # Don't reuse the args.
890 return self.update(options, [], file_list)
[email protected]fb2b8eb2009-04-23 21:03:42891
[email protected]df7a3132009-05-12 17:49:49892 files = CaptureSVNStatus(path)
[email protected]fb2b8eb2009-04-23 21:03:42893 # Batch the command.
894 files_to_revert = []
895 for file in files:
[email protected]4810a962009-05-12 21:03:34896 file_path = os.path.join(path, file[1])
[email protected]df7a3132009-05-12 17:49:49897 print(file_path)
[email protected]fb2b8eb2009-04-23 21:03:42898 # Unversioned file or unexpected unversioned file.
[email protected]4810a962009-05-12 21:03:34899 if file[0][0] in ('?', '~'):
[email protected]fb2b8eb2009-04-23 21:03:42900 # Remove extraneous file. Also remove unexpected unversioned
901 # directories. svn won't touch them but we want to delete these.
902 file_list.append(file_path)
903 try:
904 os.remove(file_path)
905 except EnvironmentError:
906 RemoveDirectory(file_path)
907
[email protected]4810a962009-05-12 21:03:34908 if file[0][0] != '?':
[email protected]fb2b8eb2009-04-23 21:03:42909 # For any other status, svn revert will work.
910 file_list.append(file_path)
[email protected]4810a962009-05-12 21:03:34911 files_to_revert.append(file[1])
[email protected]fb2b8eb2009-04-23 21:03:42912
913 # Revert them all at once.
914 if files_to_revert:
915 accumulated_paths = []
916 accumulated_length = 0
917 command = ['revert']
918 for p in files_to_revert:
919 # Some shell have issues with command lines too long.
920 if accumulated_length and accumulated_length + len(p) > 3072:
[email protected]df7a3132009-05-12 17:49:49921 RunSVN(command + accumulated_paths,
[email protected]fb2b8eb2009-04-23 21:03:42922 os.path.join(self._root_dir, self.relpath))
923 accumulated_paths = []
924 accumulated_length = 0
925 else:
926 accumulated_paths.append(p)
927 accumulated_length += len(p)
928 if accumulated_paths:
[email protected]df7a3132009-05-12 17:49:49929 RunSVN(command + accumulated_paths,
[email protected]fb2b8eb2009-04-23 21:03:42930 os.path.join(self._root_dir, self.relpath))
931
932 def status(self, options, args, file_list):
933 """Display status information."""
[email protected]edd27d12009-05-01 17:46:56934 path = os.path.join(self._root_dir, self.relpath)
[email protected]fb2b8eb2009-04-23 21:03:42935 command = ['status']
936 command.extend(args)
[email protected]edd27d12009-05-01 17:46:56937 if not os.path.isdir(path):
938 # svn status won't work if the directory doesn't exist.
[email protected]df7a3132009-05-12 17:49:49939 print("\n________ couldn't run \'%s\' in \'%s\':\nThe directory "
940 "does not exist."
941 % (' '.join(command), path))
[email protected]edd27d12009-05-01 17:46:56942 # There's no file list to retrieve.
943 else:
[email protected]df7a3132009-05-12 17:49:49944 RunSVNAndGetFileList(command, path, file_list)
[email protected]fb2b8eb2009-04-23 21:03:42945
946
947## GClient implementation.
948
949
950class GClient(object):
951 """Object that represent a gclient checkout."""
952
953 supported_commands = [
[email protected]644aa0c2009-07-17 20:20:41954 'cleanup', 'diff', 'export', 'revert', 'status', 'update', 'runhooks'
[email protected]fb2b8eb2009-04-23 21:03:42955 ]
956
957 def __init__(self, root_dir, options):
958 self._root_dir = root_dir
959 self._options = options
960 self._config_content = None
961 self._config_dict = {}
962 self._deps_hooks = []
963
964 def SetConfig(self, content):
965 self._config_dict = {}
966 self._config_content = content
[email protected]df0032c2009-05-29 10:43:56967 try:
968 exec(content, self._config_dict)
969 except SyntaxError, e:
970 try:
971 # Try to construct a human readable error message
972 error_message = [
973 'There is a syntax error in your configuration file.',
974 'Line #%s, character %s:' % (e.lineno, e.offset),
975 '"%s"' % re.sub(r'[\r\n]*$', '', e.text) ]
976 except:
977 # Something went wrong, re-raise the original exception
978 raise e
979 else:
980 # Raise a new exception with the human readable message:
981 raise Error('\n'.join(error_message))
[email protected]fb2b8eb2009-04-23 21:03:42982
983 def SaveConfig(self):
984 FileWrite(os.path.join(self._root_dir, self._options.config_filename),
985 self._config_content)
986
987 def _LoadConfig(self):
988 client_source = FileRead(os.path.join(self._root_dir,
989 self._options.config_filename))
990 self.SetConfig(client_source)
991
992 def ConfigContent(self):
993 return self._config_content
994
995 def GetVar(self, key, default=None):
996 return self._config_dict.get(key, default)
997
998 @staticmethod
999 def LoadCurrentConfig(options, from_dir=None):
1000 """Searches for and loads a .gclient file relative to the current working
1001 dir.
1002
1003 Returns:
1004 A dict representing the contents of the .gclient file or an empty dict if
1005 the .gclient file doesn't exist.
1006 """
1007 if not from_dir:
1008 from_dir = os.curdir
1009 path = os.path.realpath(from_dir)
[email protected]0329e672009-05-13 18:41:041010 while not os.path.exists(os.path.join(path, options.config_filename)):
[email protected]fb2b8eb2009-04-23 21:03:421011 next = os.path.split(path)
1012 if not next[1]:
1013 return None
1014 path = next[0]
[email protected]2806acc2009-05-15 12:33:341015 client = GClient(path, options)
[email protected]fb2b8eb2009-04-23 21:03:421016 client._LoadConfig()
1017 return client
1018
1019 def SetDefaultConfig(self, solution_name, solution_url, safesync_url):
1020 self.SetConfig(DEFAULT_CLIENT_FILE_TEXT % (
1021 solution_name, solution_url, safesync_url
1022 ))
1023
1024 def _SaveEntries(self, entries):
1025 """Creates a .gclient_entries file to record the list of unique checkouts.
1026
1027 The .gclient_entries file lives in the same directory as .gclient.
1028
1029 Args:
1030 entries: A sequence of solution names.
1031 """
1032 text = "entries = [\n"
1033 for entry in entries:
1034 text += " \"%s\",\n" % entry
1035 text += "]\n"
1036 FileWrite(os.path.join(self._root_dir, self._options.entries_filename),
1037 text)
1038
1039 def _ReadEntries(self):
1040 """Read the .gclient_entries file for the given client.
1041
1042 Args:
1043 client: The client for which the entries file should be read.
1044
1045 Returns:
1046 A sequence of solution names, which will be empty if there is the
1047 entries file hasn't been created yet.
1048 """
1049 scope = {}
1050 filename = os.path.join(self._root_dir, self._options.entries_filename)
[email protected]0329e672009-05-13 18:41:041051 if not os.path.exists(filename):
[email protected]fb2b8eb2009-04-23 21:03:421052 return []
1053 exec(FileRead(filename), scope)
1054 return scope["entries"]
1055
1056 class FromImpl:
1057 """Used to implement the From syntax."""
1058
1059 def __init__(self, module_name):
1060 self.module_name = module_name
1061
1062 def __str__(self):
1063 return 'From("%s")' % self.module_name
1064
1065 class _VarImpl:
1066 def __init__(self, custom_vars, local_scope):
1067 self._custom_vars = custom_vars
1068 self._local_scope = local_scope
1069
1070 def Lookup(self, var_name):
1071 """Implements the Var syntax."""
1072 if var_name in self._custom_vars:
1073 return self._custom_vars[var_name]
1074 elif var_name in self._local_scope.get("vars", {}):
1075 return self._local_scope["vars"][var_name]
1076 raise Error("Var is not defined: %s" % var_name)
1077
1078 def _ParseSolutionDeps(self, solution_name, solution_deps_content,
1079 custom_vars):
1080 """Parses the DEPS file for the specified solution.
1081
1082 Args:
1083 solution_name: The name of the solution to query.
1084 solution_deps_content: Content of the DEPS file for the solution
1085 custom_vars: A dict of vars to override any vars defined in the DEPS file.
1086
1087 Returns:
1088 A dict mapping module names (as relative paths) to URLs or an empty
1089 dict if the solution does not have a DEPS file.
1090 """
1091 # Skip empty
1092 if not solution_deps_content:
1093 return {}
1094 # Eval the content
1095 local_scope = {}
1096 var = self._VarImpl(custom_vars, local_scope)
1097 global_scope = {"From": self.FromImpl, "Var": var.Lookup, "deps_os": {}}
1098 exec(solution_deps_content, global_scope, local_scope)
1099 deps = local_scope.get("deps", {})
1100
1101 # load os specific dependencies if defined. these dependencies may
1102 # override or extend the values defined by the 'deps' member.
1103 if "deps_os" in local_scope:
1104 deps_os_choices = {
1105 "win32": "win",
1106 "win": "win",
1107 "cygwin": "win",
1108 "darwin": "mac",
1109 "mac": "mac",
1110 "unix": "unix",
1111 "linux": "unix",
1112 "linux2": "unix",
1113 }
1114
1115 if self._options.deps_os is not None:
1116 deps_to_include = self._options.deps_os.split(",")
1117 if "all" in deps_to_include:
1118 deps_to_include = deps_os_choices.values()
1119 else:
1120 deps_to_include = [deps_os_choices.get(self._options.platform, "unix")]
1121
1122 deps_to_include = set(deps_to_include)
1123 for deps_os_key in deps_to_include:
1124 os_deps = local_scope["deps_os"].get(deps_os_key, {})
1125 if len(deps_to_include) > 1:
1126 # Ignore any overrides when including deps for more than one
1127 # platform, so we collect the broadest set of dependencies available.
1128 # We may end up with the wrong revision of something for our
1129 # platform, but this is the best we can do.
1130 deps.update([x for x in os_deps.items() if not x[0] in deps])
1131 else:
1132 deps.update(os_deps)
1133
1134 if 'hooks' in local_scope:
1135 self._deps_hooks.extend(local_scope['hooks'])
1136
1137 # If use_relative_paths is set in the DEPS file, regenerate
1138 # the dictionary using paths relative to the directory containing
1139 # the DEPS file.
1140 if local_scope.get('use_relative_paths'):
1141 rel_deps = {}
1142 for d, url in deps.items():
1143 # normpath is required to allow DEPS to use .. in their
1144 # dependency local path.
1145 rel_deps[os.path.normpath(os.path.join(solution_name, d))] = url
1146 return rel_deps
1147 else:
1148 return deps
1149
1150 def _ParseAllDeps(self, solution_urls, solution_deps_content):
1151 """Parse the complete list of dependencies for the client.
1152
1153 Args:
1154 solution_urls: A dict mapping module names (as relative paths) to URLs
1155 corresponding to the solutions specified by the client. This parameter
1156 is passed as an optimization.
1157 solution_deps_content: A dict mapping module names to the content
1158 of their DEPS files
1159
1160 Returns:
1161 A dict mapping module names (as relative paths) to URLs corresponding
1162 to the entire set of dependencies to checkout for the given client.
1163
1164 Raises:
1165 Error: If a dependency conflicts with another dependency or of a solution.
1166 """
1167 deps = {}
1168 for solution in self.GetVar("solutions"):
1169 custom_vars = solution.get("custom_vars", {})
1170 solution_deps = self._ParseSolutionDeps(
1171 solution["name"],
1172 solution_deps_content[solution["name"]],
1173 custom_vars)
1174
1175 # If a line is in custom_deps, but not in the solution, we want to append
1176 # this line to the solution.
1177 if "custom_deps" in solution:
1178 for d in solution["custom_deps"]:
1179 if d not in solution_deps:
1180 solution_deps[d] = solution["custom_deps"][d]
1181
1182 for d in solution_deps:
1183 if "custom_deps" in solution and d in solution["custom_deps"]:
1184 # Dependency is overriden.
1185 url = solution["custom_deps"][d]
1186 if url is None:
1187 continue
1188 else:
1189 url = solution_deps[d]
1190 # if we have a From reference dependent on another solution, then
1191 # just skip the From reference. When we pull deps for the solution,
1192 # we will take care of this dependency.
1193 #
1194 # If multiple solutions all have the same From reference, then we
1195 # should only add one to our list of dependencies.
1196 if type(url) != str:
1197 if url.module_name in solution_urls:
1198 # Already parsed.
1199 continue
1200 if d in deps and type(deps[d]) != str:
1201 if url.module_name == deps[d].module_name:
1202 continue
1203 else:
1204 parsed_url = urlparse.urlparse(url)
1205 scheme = parsed_url[0]
1206 if not scheme:
1207 # A relative url. Fetch the real base.
1208 path = parsed_url[2]
1209 if path[0] != "/":
1210 raise Error(
1211 "relative DEPS entry \"%s\" must begin with a slash" % d)
1212 # Create a scm just to query the full url.
[email protected]2806acc2009-05-15 12:33:341213 scm = SCMWrapper(solution["url"], self._root_dir, None)
[email protected]fb2b8eb2009-04-23 21:03:421214 url = scm.FullUrlForRelativeUrl(url)
1215 if d in deps and deps[d] != url:
1216 raise Error(
1217 "Solutions have conflicting versions of dependency \"%s\"" % d)
1218 if d in solution_urls and solution_urls[d] != url:
1219 raise Error(
1220 "Dependency \"%s\" conflicts with specified solution" % d)
1221 # Grab the dependency.
1222 deps[d] = url
1223 return deps
1224
[email protected]71b40682009-07-31 23:40:091225 def _RunHookAction(self, hook_dict, matching_file_list):
[email protected]fb2b8eb2009-04-23 21:03:421226 """Runs the action from a single hook.
1227 """
1228 command = hook_dict['action'][:]
1229 if command[0] == 'python':
1230 # If the hook specified "python" as the first item, the action is a
1231 # Python script. Run it by starting a new copy of the same
1232 # interpreter.
1233 command[0] = sys.executable
1234
[email protected]71b40682009-07-31 23:40:091235 if '$matching_files' in command:
[email protected]68f2e092009-08-06 17:05:351236 splice_index = command.index('$matching_files')
1237 command[splice_index:splice_index + 1] = matching_file_list
[email protected]71b40682009-07-31 23:40:091238
[email protected]fb2b8eb2009-04-23 21:03:421239 # Use a discrete exit status code of 2 to indicate that a hook action
1240 # failed. Users of this script may wish to treat hook action failures
1241 # differently from VC failures.
[email protected]df7a3132009-05-12 17:49:491242 SubprocessCall(command, self._root_dir, fail_status=2)
[email protected]fb2b8eb2009-04-23 21:03:421243
1244 def _RunHooks(self, command, file_list, is_using_git):
1245 """Evaluates all hooks, running actions as needed.
1246 """
1247 # Hooks only run for these command types.
1248 if not command in ('update', 'revert', 'runhooks'):
1249 return
1250
[email protected]67820ef2009-07-27 17:23:001251 # Hooks only run when --nohooks is not specified
1252 if self._options.nohooks:
1253 return
1254
[email protected]fb2b8eb2009-04-23 21:03:421255 # Get any hooks from the .gclient file.
1256 hooks = self.GetVar("hooks", [])
1257 # Add any hooks found in DEPS files.
1258 hooks.extend(self._deps_hooks)
1259
1260 # If "--force" was specified, run all hooks regardless of what files have
1261 # changed. If the user is using git, then we don't know what files have
1262 # changed so we always run all hooks.
1263 if self._options.force or is_using_git:
1264 for hook_dict in hooks:
[email protected]71b40682009-07-31 23:40:091265 self._RunHookAction(hook_dict, [])
[email protected]fb2b8eb2009-04-23 21:03:421266 return
1267
1268 # Run hooks on the basis of whether the files from the gclient operation
1269 # match each hook's pattern.
1270 for hook_dict in hooks:
1271 pattern = re.compile(hook_dict['pattern'])
[email protected]71b40682009-07-31 23:40:091272 matching_file_list = [file for file in file_list if pattern.search(file)]
1273 if matching_file_list:
1274 self._RunHookAction(hook_dict, matching_file_list)
[email protected]fb2b8eb2009-04-23 21:03:421275
1276 def RunOnDeps(self, command, args):
1277 """Runs a command on each dependency in a client and its dependencies.
1278
1279 The module's dependencies are specified in its top-level DEPS files.
1280
1281 Args:
1282 command: The command to use (e.g., 'status' or 'diff')
1283 args: list of str - extra arguments to add to the command line.
1284
1285 Raises:
1286 Error: If the client has conflicting entries.
1287 """
1288 if not command in self.supported_commands:
1289 raise Error("'%s' is an unsupported command" % command)
1290
1291 # Check for revision overrides.
1292 revision_overrides = {}
1293 for revision in self._options.revisions:
1294 if revision.find("@") == -1:
1295 raise Error(
1296 "Specify the full dependency when specifying a revision number.")
1297 revision_elem = revision.split("@")
1298 # Disallow conflicting revs
1299 if revision_overrides.has_key(revision_elem[0]) and \
1300 revision_overrides[revision_elem[0]] != revision_elem[1]:
1301 raise Error(
1302 "Conflicting revision numbers specified.")
1303 revision_overrides[revision_elem[0]] = revision_elem[1]
1304
1305 solutions = self.GetVar("solutions")
1306 if not solutions:
1307 raise Error("No solution specified")
1308
1309 # When running runhooks --force, there's no need to consult the SCM.
1310 # All known hooks are expected to run unconditionally regardless of working
1311 # copy state, so skip the SCM status check.
1312 run_scm = not (command == 'runhooks' and self._options.force)
1313
1314 entries = {}
1315 entries_deps_content = {}
1316 file_list = []
1317 # Run on the base solutions first.
1318 for solution in solutions:
1319 name = solution["name"]
1320 if name in entries:
1321 raise Error("solution %s specified more than once" % name)
1322 url = solution["url"]
1323 entries[name] = url
1324 if run_scm:
1325 self._options.revision = revision_overrides.get(name)
[email protected]2806acc2009-05-15 12:33:341326 scm = SCMWrapper(url, self._root_dir, name)
[email protected]fb2b8eb2009-04-23 21:03:421327 scm.RunCommand(command, self._options, args, file_list)
1328 self._options.revision = None
1329 try:
1330 deps_content = FileRead(os.path.join(self._root_dir, name,
1331 self._options.deps_file))
1332 except IOError, e:
1333 if e.errno != errno.ENOENT:
1334 raise
1335 deps_content = ""
1336 entries_deps_content[name] = deps_content
1337
1338 # Process the dependencies next (sort alphanumerically to ensure that
1339 # containing directories get populated first and for readability)
1340 deps = self._ParseAllDeps(entries, entries_deps_content)
1341 deps_to_process = deps.keys()
1342 deps_to_process.sort()
1343
1344 # First pass for direct dependencies.
1345 for d in deps_to_process:
1346 if type(deps[d]) == str:
1347 url = deps[d]
1348 entries[d] = url
1349 if run_scm:
1350 self._options.revision = revision_overrides.get(d)
[email protected]2806acc2009-05-15 12:33:341351 scm = SCMWrapper(url, self._root_dir, d)
[email protected]fb2b8eb2009-04-23 21:03:421352 scm.RunCommand(command, self._options, args, file_list)
1353 self._options.revision = None
1354
1355 # Second pass for inherited deps (via the From keyword)
1356 for d in deps_to_process:
1357 if type(deps[d]) != str:
1358 sub_deps = self._ParseSolutionDeps(
1359 deps[d].module_name,
1360 FileRead(os.path.join(self._root_dir,
1361 deps[d].module_name,
1362 self._options.deps_file)),
1363 {})
1364 url = sub_deps[d]
1365 entries[d] = url
1366 if run_scm:
1367 self._options.revision = revision_overrides.get(d)
[email protected]2806acc2009-05-15 12:33:341368 scm = SCMWrapper(url, self._root_dir, d)
[email protected]fb2b8eb2009-04-23 21:03:421369 scm.RunCommand(command, self._options, args, file_list)
1370 self._options.revision = None
1371
1372 is_using_git = IsUsingGit(self._root_dir, entries.keys())
1373 self._RunHooks(command, file_list, is_using_git)
1374
1375 if command == 'update':
[email protected]cdcee802009-06-23 15:30:421376 # Notify the user if there is an orphaned entry in their working copy.
1377 # Only delete the directory if there are no changes in it, and
1378 # delete_unversioned_trees is set to true.
[email protected]fb2b8eb2009-04-23 21:03:421379 prev_entries = self._ReadEntries()
1380 for entry in prev_entries:
[email protected]c5e9aec2009-08-03 18:25:561381 # Fix path separator on Windows.
1382 entry_fixed = entry.replace('/', os.path.sep)
1383 e_dir = os.path.join(self._root_dir, entry_fixed)
1384 # Use entry and not entry_fixed there.
[email protected]0329e672009-05-13 18:41:041385 if entry not in entries and os.path.exists(e_dir):
[email protected]8399dc02009-06-23 21:36:251386 if not self._options.delete_unversioned_trees or \
1387 CaptureSVNStatus(e_dir):
[email protected]c5e9aec2009-08-03 18:25:561388 # There are modified files in this entry. Keep warning until
1389 # removed.
1390 entries[entry] = None
1391 print(("\nWARNING: \"%s\" is no longer part of this client. "
1392 "It is recommended that you manually remove it.\n") %
1393 entry_fixed)
[email protected]fb2b8eb2009-04-23 21:03:421394 else:
1395 # Delete the entry
[email protected]df7a3132009-05-12 17:49:491396 print("\n________ deleting \'%s\' " +
[email protected]c5e9aec2009-08-03 18:25:561397 "in \'%s\'") % (entry_fixed, self._root_dir)
[email protected]fb2b8eb2009-04-23 21:03:421398 RemoveDirectory(e_dir)
1399 # record the current list of entries for next time
1400 self._SaveEntries(entries)
1401
1402 def PrintRevInfo(self):
1403 """Output revision info mapping for the client and its dependencies. This
1404 allows the capture of a overall "revision" for the source tree that can
1405 be used to reproduce the same tree in the future. The actual output
1406 contains enough information (source paths, svn server urls and revisions)
1407 that it can be used either to generate external svn commands (without
1408 gclient) or as input to gclient's --rev option (with some massaging of
1409 the data).
1410
1411 NOTE: Unlike RunOnDeps this does not require a local checkout and is run
1412 on the Pulse master. It MUST NOT execute hooks.
1413
1414 Raises:
1415 Error: If the client has conflicting entries.
1416 """
1417 # Check for revision overrides.
1418 revision_overrides = {}
1419 for revision in self._options.revisions:
1420 if revision.find("@") < 0:
1421 raise Error(
1422 "Specify the full dependency when specifying a revision number.")
1423 revision_elem = revision.split("@")
1424 # Disallow conflicting revs
1425 if revision_overrides.has_key(revision_elem[0]) and \
1426 revision_overrides[revision_elem[0]] != revision_elem[1]:
1427 raise Error(
1428 "Conflicting revision numbers specified.")
1429 revision_overrides[revision_elem[0]] = revision_elem[1]
1430
1431 solutions = self.GetVar("solutions")
1432 if not solutions:
1433 raise Error("No solution specified")
1434
1435 entries = {}
1436 entries_deps_content = {}
1437
1438 # Inner helper to generate base url and rev tuple (including honoring
1439 # |revision_overrides|)
1440 def GetURLAndRev(name, original_url):
1441 if original_url.find("@") < 0:
1442 if revision_overrides.has_key(name):
1443 return (original_url, int(revision_overrides[name]))
1444 else:
1445 # TODO(aharper): SVN/SCMWrapper cleanup (non-local commandset)
[email protected]df7a3132009-05-12 17:49:491446 return (original_url, CaptureSVNHeadRevision(original_url))
[email protected]fb2b8eb2009-04-23 21:03:421447 else:
1448 url_components = original_url.split("@")
1449 if revision_overrides.has_key(name):
1450 return (url_components[0], int(revision_overrides[name]))
1451 else:
1452 return (url_components[0], int(url_components[1]))
1453
1454 # Run on the base solutions first.
1455 for solution in solutions:
1456 name = solution["name"]
1457 if name in entries:
1458 raise Error("solution %s specified more than once" % name)
1459 (url, rev) = GetURLAndRev(name, solution["url"])
1460 entries[name] = "%s@%d" % (url, rev)
1461 # TODO(aharper): SVN/SCMWrapper cleanup (non-local commandset)
1462 entries_deps_content[name] = CaptureSVN(
[email protected]fb2b8eb2009-04-23 21:03:421463 ["cat",
1464 "%s/%s@%d" % (url,
1465 self._options.deps_file,
1466 rev)],
1467 os.getcwd())
1468
1469 # Process the dependencies next (sort alphanumerically to ensure that
1470 # containing directories get populated first and for readability)
1471 deps = self._ParseAllDeps(entries, entries_deps_content)
1472 deps_to_process = deps.keys()
1473 deps_to_process.sort()
1474
1475 # First pass for direct dependencies.
1476 for d in deps_to_process:
1477 if type(deps[d]) == str:
1478 (url, rev) = GetURLAndRev(d, deps[d])
1479 entries[d] = "%s@%d" % (url, rev)
1480
1481 # Second pass for inherited deps (via the From keyword)
1482 for d in deps_to_process:
1483 if type(deps[d]) != str:
1484 deps_parent_url = entries[deps[d].module_name]
1485 if deps_parent_url.find("@") < 0:
1486 raise Error("From %s missing revisioned url" % deps[d].module_name)
1487 deps_parent_url_components = deps_parent_url.split("@")
1488 # TODO(aharper): SVN/SCMWrapper cleanup (non-local commandset)
1489 deps_parent_content = CaptureSVN(
[email protected]fb2b8eb2009-04-23 21:03:421490 ["cat",
1491 "%s/%s@%s" % (deps_parent_url_components[0],
1492 self._options.deps_file,
1493 deps_parent_url_components[1])],
1494 os.getcwd())
1495 sub_deps = self._ParseSolutionDeps(
1496 deps[d].module_name,
1497 FileRead(os.path.join(self._root_dir,
1498 deps[d].module_name,
1499 self._options.deps_file)),
1500 {})
1501 (url, rev) = GetURLAndRev(d, sub_deps[d])
1502 entries[d] = "%s@%d" % (url, rev)
1503
[email protected]df7a3132009-05-12 17:49:491504 print(";".join(["%s,%s" % (x, entries[x]) for x in sorted(entries.keys())]))
[email protected]fb2b8eb2009-04-23 21:03:421505
1506
1507## gclient commands.
1508
1509
1510def DoCleanup(options, args):
1511 """Handle the cleanup subcommand.
1512
1513 Raises:
1514 Error: if client isn't configured properly.
1515 """
[email protected]2806acc2009-05-15 12:33:341516 client = GClient.LoadCurrentConfig(options)
[email protected]fb2b8eb2009-04-23 21:03:421517 if not client:
1518 raise Error("client not configured; see 'gclient config'")
1519 if options.verbose:
1520 # Print out the .gclient file. This is longer than if we just printed the
1521 # client dict, but more legible, and it might contain helpful comments.
[email protected]df7a3132009-05-12 17:49:491522 print(client.ConfigContent())
[email protected]fb2b8eb2009-04-23 21:03:421523 options.verbose = True
1524 return client.RunOnDeps('cleanup', args)
1525
1526
1527def DoConfig(options, args):
1528 """Handle the config subcommand.
1529
1530 Args:
1531 options: If options.spec set, a string providing contents of config file.
1532 args: The command line args. If spec is not set,
1533 then args[0] is a string URL to get for config file.
1534
1535 Raises:
1536 Error: on usage error
1537 """
1538 if len(args) < 1 and not options.spec:
1539 raise Error("required argument missing; see 'gclient help config'")
[email protected]0329e672009-05-13 18:41:041540 if os.path.exists(options.config_filename):
[email protected]fb2b8eb2009-04-23 21:03:421541 raise Error("%s file already exists in the current directory" %
1542 options.config_filename)
[email protected]2806acc2009-05-15 12:33:341543 client = GClient('.', options)
[email protected]fb2b8eb2009-04-23 21:03:421544 if options.spec:
1545 client.SetConfig(options.spec)
1546 else:
1547 # TODO(darin): it would be nice to be able to specify an alternate relpath
1548 # for the given URL.
[email protected]1ab7ffc2009-06-03 17:21:371549 base_url = args[0].rstrip('/')
1550 name = base_url.split("/")[-1]
[email protected]fb2b8eb2009-04-23 21:03:421551 safesync_url = ""
1552 if len(args) > 1:
1553 safesync_url = args[1]
1554 client.SetDefaultConfig(name, base_url, safesync_url)
1555 client.SaveConfig()
1556
1557
[email protected]644aa0c2009-07-17 20:20:411558def DoExport(options, args):
1559 """Handle the export subcommand.
[email protected]71b40682009-07-31 23:40:091560
[email protected]644aa0c2009-07-17 20:20:411561 Raises:
1562 Error: on usage error
1563 """
1564 if len(args) != 1:
1565 raise Error("Need directory name")
1566 client = GClient.LoadCurrentConfig(options)
1567
1568 if not client:
1569 raise Error("client not configured; see 'gclient config'")
1570
1571 if options.verbose:
1572 # Print out the .gclient file. This is longer than if we just printed the
1573 # client dict, but more legible, and it might contain helpful comments.
1574 print(client.ConfigContent())
1575 return client.RunOnDeps('export', args)
1576
[email protected]fb2b8eb2009-04-23 21:03:421577def DoHelp(options, args):
1578 """Handle the help subcommand giving help for another subcommand.
1579
1580 Raises:
1581 Error: if the command is unknown.
1582 """
1583 if len(args) == 1 and args[0] in COMMAND_USAGE_TEXT:
[email protected]df7a3132009-05-12 17:49:491584 print(COMMAND_USAGE_TEXT[args[0]])
[email protected]fb2b8eb2009-04-23 21:03:421585 else:
1586 raise Error("unknown subcommand '%s'; see 'gclient help'" % args[0])
1587
1588
1589def DoStatus(options, args):
1590 """Handle the status subcommand.
1591
1592 Raises:
1593 Error: if client isn't configured properly.
1594 """
[email protected]2806acc2009-05-15 12:33:341595 client = GClient.LoadCurrentConfig(options)
[email protected]fb2b8eb2009-04-23 21:03:421596 if not client:
1597 raise Error("client not configured; see 'gclient config'")
1598 if options.verbose:
1599 # Print out the .gclient file. This is longer than if we just printed the
1600 # client dict, but more legible, and it might contain helpful comments.
[email protected]df7a3132009-05-12 17:49:491601 print(client.ConfigContent())
[email protected]fb2b8eb2009-04-23 21:03:421602 options.verbose = True
1603 return client.RunOnDeps('status', args)
1604
1605
1606def DoUpdate(options, args):
1607 """Handle the update and sync subcommands.
1608
1609 Raises:
1610 Error: if client isn't configured properly.
1611 """
[email protected]2806acc2009-05-15 12:33:341612 client = GClient.LoadCurrentConfig(options)
[email protected]fb2b8eb2009-04-23 21:03:421613
1614 if not client:
1615 raise Error("client not configured; see 'gclient config'")
1616
1617 if not options.head:
1618 solutions = client.GetVar('solutions')
1619 if solutions:
1620 for s in solutions:
1621 if s.get('safesync_url', ''):
1622 # rip through revisions and make sure we're not over-riding
1623 # something that was explicitly passed
1624 has_key = False
1625 for r in options.revisions:
1626 if r.split('@')[0] == s['name']:
1627 has_key = True
1628 break
1629
1630 if not has_key:
1631 handle = urllib.urlopen(s['safesync_url'])
1632 rev = handle.read().strip()
1633 handle.close()
1634 if len(rev):
1635 options.revisions.append(s['name']+'@'+rev)
1636
1637 if options.verbose:
1638 # Print out the .gclient file. This is longer than if we just printed the
1639 # client dict, but more legible, and it might contain helpful comments.
[email protected]df7a3132009-05-12 17:49:491640 print(client.ConfigContent())
[email protected]fb2b8eb2009-04-23 21:03:421641 return client.RunOnDeps('update', args)
1642
1643
1644def DoDiff(options, args):
1645 """Handle the diff subcommand.
1646
1647 Raises:
1648 Error: if client isn't configured properly.
1649 """
[email protected]2806acc2009-05-15 12:33:341650 client = GClient.LoadCurrentConfig(options)
[email protected]fb2b8eb2009-04-23 21:03:421651 if not client:
1652 raise Error("client not configured; see 'gclient config'")
1653 if options.verbose:
1654 # Print out the .gclient file. This is longer than if we just printed the
1655 # client dict, but more legible, and it might contain helpful comments.
[email protected]df7a3132009-05-12 17:49:491656 print(client.ConfigContent())
[email protected]fb2b8eb2009-04-23 21:03:421657 options.verbose = True
1658 return client.RunOnDeps('diff', args)
1659
1660
1661def DoRevert(options, args):
1662 """Handle the revert subcommand.
1663
1664 Raises:
1665 Error: if client isn't configured properly.
1666 """
[email protected]2806acc2009-05-15 12:33:341667 client = GClient.LoadCurrentConfig(options)
[email protected]fb2b8eb2009-04-23 21:03:421668 if not client:
1669 raise Error("client not configured; see 'gclient config'")
1670 return client.RunOnDeps('revert', args)
1671
1672
1673def DoRunHooks(options, args):
1674 """Handle the runhooks subcommand.
1675
1676 Raises:
1677 Error: if client isn't configured properly.
1678 """
[email protected]2806acc2009-05-15 12:33:341679 client = GClient.LoadCurrentConfig(options)
[email protected]fb2b8eb2009-04-23 21:03:421680 if not client:
1681 raise Error("client not configured; see 'gclient config'")
1682 if options.verbose:
1683 # Print out the .gclient file. This is longer than if we just printed the
1684 # client dict, but more legible, and it might contain helpful comments.
[email protected]df7a3132009-05-12 17:49:491685 print(client.ConfigContent())
[email protected]fb2b8eb2009-04-23 21:03:421686 return client.RunOnDeps('runhooks', args)
1687
1688
1689def DoRevInfo(options, args):
1690 """Handle the revinfo subcommand.
1691
1692 Raises:
1693 Error: if client isn't configured properly.
1694 """
[email protected]2806acc2009-05-15 12:33:341695 client = GClient.LoadCurrentConfig(options)
[email protected]fb2b8eb2009-04-23 21:03:421696 if not client:
1697 raise Error("client not configured; see 'gclient config'")
1698 client.PrintRevInfo()
1699
1700
1701gclient_command_map = {
1702 "cleanup": DoCleanup,
1703 "config": DoConfig,
1704 "diff": DoDiff,
[email protected]644aa0c2009-07-17 20:20:411705 "export": DoExport,
[email protected]fb2b8eb2009-04-23 21:03:421706 "help": DoHelp,
1707 "status": DoStatus,
1708 "sync": DoUpdate,
1709 "update": DoUpdate,
1710 "revert": DoRevert,
1711 "runhooks": DoRunHooks,
1712 "revinfo" : DoRevInfo,
1713}
1714
1715
1716def DispatchCommand(command, options, args, command_map=None):
1717 """Dispatches the appropriate subcommand based on command line arguments."""
1718 if command_map is None:
1719 command_map = gclient_command_map
1720
1721 if command in command_map:
1722 return command_map[command](options, args)
1723 else:
1724 raise Error("unknown subcommand '%s'; see 'gclient help'" % command)
1725
1726
1727def Main(argv):
1728 """Parse command line arguments and dispatch command."""
1729
1730 option_parser = optparse.OptionParser(usage=DEFAULT_USAGE_TEXT,
1731 version=__version__)
1732 option_parser.disable_interspersed_args()
1733 option_parser.add_option("", "--force", action="store_true", default=False,
1734 help=("(update/sync only) force update even "
1735 "for modules which haven't changed"))
[email protected]67820ef2009-07-27 17:23:001736 option_parser.add_option("", "--nohooks", action="store_true", default=False,
1737 help=("(update/sync/revert only) prevent the hooks from "
1738 "running"))
[email protected]fb2b8eb2009-04-23 21:03:421739 option_parser.add_option("", "--revision", action="append", dest="revisions",
1740 metavar="REV", default=[],
1741 help=("(update/sync only) sync to a specific "
1742 "revision, can be used multiple times for "
1743 "each solution, e.g. --revision=src@123, "
1744 "--revision=internal@32"))
1745 option_parser.add_option("", "--deps", default=None, dest="deps_os",
1746 metavar="OS_LIST",
1747 help=("(update/sync only) sync deps for the "
1748 "specified (comma-separated) platform(s); "
1749 "'all' will sync all platforms"))
1750 option_parser.add_option("", "--spec", default=None,
1751 help=("(config only) create a gclient file "
1752 "containing the provided string"))
1753 option_parser.add_option("", "--verbose", action="store_true", default=False,
1754 help="produce additional output for diagnostics")
1755 option_parser.add_option("", "--manually_grab_svn_rev", action="store_true",
1756 default=False,
1757 help="Skip svn up whenever possible by requesting "
1758 "actual HEAD revision from the repository")
1759 option_parser.add_option("", "--head", action="store_true", default=False,
1760 help=("skips any safesync_urls specified in "
1761 "configured solutions"))
[email protected]cdcee802009-06-23 15:30:421762 option_parser.add_option("", "--delete_unversioned_trees",
1763 action="store_true", default=False,
1764 help=("on update, delete any unexpected "
1765 "unversioned trees that are in the checkout"))
[email protected]fb2b8eb2009-04-23 21:03:421766
1767 if len(argv) < 2:
1768 # Users don't need to be told to use the 'help' command.
1769 option_parser.print_help()
1770 return 1
1771 # Add manual support for --version as first argument.
1772 if argv[1] == '--version':
1773 option_parser.print_version()
1774 return 0
1775
1776 # Add manual support for --help as first argument.
1777 if argv[1] == '--help':
1778 argv[1] = 'help'
1779
1780 command = argv[1]
1781 options, args = option_parser.parse_args(argv[2:])
1782
1783 if len(argv) < 3 and command == "help":
1784 option_parser.print_help()
1785 return 0
1786
1787 # Files used for configuration and state saving.
1788 options.config_filename = os.environ.get("GCLIENT_FILE", ".gclient")
1789 options.entries_filename = ".gclient_entries"
1790 options.deps_file = "DEPS"
1791
[email protected]fb2b8eb2009-04-23 21:03:421792 options.platform = sys.platform
1793 return DispatchCommand(command, options, args)
1794
1795
1796if "__main__" == __name__:
1797 try:
1798 result = Main(sys.argv)
1799 except Error, e:
[email protected]df7a3132009-05-12 17:49:491800 print >> sys.stderr, "Error: %s" % str(e)
[email protected]fb2b8eb2009-04-23 21:03:421801 result = 1
1802 sys.exit(result)
1803
1804# vim: ts=2:sw=2:tw=80:et: