blob: 71309ccba9c73eacc0bfedef92045089f4c1d584 [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)
[email protected]d83b2b22009-08-11 15:30:551328 file_list = [os.path.join(name, file.strip()) for file in file_list]
[email protected]fb2b8eb2009-04-23 21:03:421329 self._options.revision = None
1330 try:
1331 deps_content = FileRead(os.path.join(self._root_dir, name,
1332 self._options.deps_file))
1333 except IOError, e:
1334 if e.errno != errno.ENOENT:
1335 raise
1336 deps_content = ""
1337 entries_deps_content[name] = deps_content
1338
1339 # Process the dependencies next (sort alphanumerically to ensure that
1340 # containing directories get populated first and for readability)
1341 deps = self._ParseAllDeps(entries, entries_deps_content)
1342 deps_to_process = deps.keys()
1343 deps_to_process.sort()
1344
1345 # First pass for direct dependencies.
1346 for d in deps_to_process:
1347 if type(deps[d]) == str:
1348 url = deps[d]
1349 entries[d] = url
1350 if run_scm:
1351 self._options.revision = revision_overrides.get(d)
[email protected]2806acc2009-05-15 12:33:341352 scm = SCMWrapper(url, self._root_dir, d)
[email protected]fb2b8eb2009-04-23 21:03:421353 scm.RunCommand(command, self._options, args, file_list)
1354 self._options.revision = None
1355
1356 # Second pass for inherited deps (via the From keyword)
1357 for d in deps_to_process:
1358 if type(deps[d]) != str:
1359 sub_deps = self._ParseSolutionDeps(
1360 deps[d].module_name,
1361 FileRead(os.path.join(self._root_dir,
1362 deps[d].module_name,
1363 self._options.deps_file)),
1364 {})
1365 url = sub_deps[d]
1366 entries[d] = url
1367 if run_scm:
1368 self._options.revision = revision_overrides.get(d)
[email protected]2806acc2009-05-15 12:33:341369 scm = SCMWrapper(url, self._root_dir, d)
[email protected]fb2b8eb2009-04-23 21:03:421370 scm.RunCommand(command, self._options, args, file_list)
1371 self._options.revision = None
[email protected]d83b2b22009-08-11 15:30:551372
1373 # Convert all absolute paths to relative.
1374 for i in range(len(file_list)):
1375 # TODO(phajdan.jr): We should know exactly when the paths are absolute.
1376 # It depends on the command being executed (like runhooks vs sync).
1377 if not os.path.isabs(file_list[i]):
1378 continue
1379
1380 prefix = os.path.commonprefix([self._root_dir.lower(),
1381 file_list[i].lower()])
1382 file_list[i] = file_list[i][len(prefix):]
1383
1384 # Strip any leading path separators.
1385 while file_list[i].startswith('\\') or file_list[i].startswith('/'):
1386 file_list[i] = file_list[i][1:]
[email protected]fb2b8eb2009-04-23 21:03:421387
1388 is_using_git = IsUsingGit(self._root_dir, entries.keys())
1389 self._RunHooks(command, file_list, is_using_git)
1390
1391 if command == 'update':
[email protected]cdcee802009-06-23 15:30:421392 # Notify the user if there is an orphaned entry in their working copy.
1393 # Only delete the directory if there are no changes in it, and
1394 # delete_unversioned_trees is set to true.
[email protected]fb2b8eb2009-04-23 21:03:421395 prev_entries = self._ReadEntries()
1396 for entry in prev_entries:
[email protected]c5e9aec2009-08-03 18:25:561397 # Fix path separator on Windows.
1398 entry_fixed = entry.replace('/', os.path.sep)
1399 e_dir = os.path.join(self._root_dir, entry_fixed)
1400 # Use entry and not entry_fixed there.
[email protected]0329e672009-05-13 18:41:041401 if entry not in entries and os.path.exists(e_dir):
[email protected]8399dc02009-06-23 21:36:251402 if not self._options.delete_unversioned_trees or \
1403 CaptureSVNStatus(e_dir):
[email protected]c5e9aec2009-08-03 18:25:561404 # There are modified files in this entry. Keep warning until
1405 # removed.
1406 entries[entry] = None
1407 print(("\nWARNING: \"%s\" is no longer part of this client. "
1408 "It is recommended that you manually remove it.\n") %
1409 entry_fixed)
[email protected]fb2b8eb2009-04-23 21:03:421410 else:
1411 # Delete the entry
[email protected]df7a3132009-05-12 17:49:491412 print("\n________ deleting \'%s\' " +
[email protected]c5e9aec2009-08-03 18:25:561413 "in \'%s\'") % (entry_fixed, self._root_dir)
[email protected]fb2b8eb2009-04-23 21:03:421414 RemoveDirectory(e_dir)
1415 # record the current list of entries for next time
1416 self._SaveEntries(entries)
1417
1418 def PrintRevInfo(self):
1419 """Output revision info mapping for the client and its dependencies. This
1420 allows the capture of a overall "revision" for the source tree that can
1421 be used to reproduce the same tree in the future. The actual output
1422 contains enough information (source paths, svn server urls and revisions)
1423 that it can be used either to generate external svn commands (without
1424 gclient) or as input to gclient's --rev option (with some massaging of
1425 the data).
1426
1427 NOTE: Unlike RunOnDeps this does not require a local checkout and is run
1428 on the Pulse master. It MUST NOT execute hooks.
1429
1430 Raises:
1431 Error: If the client has conflicting entries.
1432 """
1433 # Check for revision overrides.
1434 revision_overrides = {}
1435 for revision in self._options.revisions:
1436 if revision.find("@") < 0:
1437 raise Error(
1438 "Specify the full dependency when specifying a revision number.")
1439 revision_elem = revision.split("@")
1440 # Disallow conflicting revs
1441 if revision_overrides.has_key(revision_elem[0]) and \
1442 revision_overrides[revision_elem[0]] != revision_elem[1]:
1443 raise Error(
1444 "Conflicting revision numbers specified.")
1445 revision_overrides[revision_elem[0]] = revision_elem[1]
1446
1447 solutions = self.GetVar("solutions")
1448 if not solutions:
1449 raise Error("No solution specified")
1450
1451 entries = {}
1452 entries_deps_content = {}
1453
1454 # Inner helper to generate base url and rev tuple (including honoring
1455 # |revision_overrides|)
1456 def GetURLAndRev(name, original_url):
1457 if original_url.find("@") < 0:
1458 if revision_overrides.has_key(name):
1459 return (original_url, int(revision_overrides[name]))
1460 else:
1461 # TODO(aharper): SVN/SCMWrapper cleanup (non-local commandset)
[email protected]df7a3132009-05-12 17:49:491462 return (original_url, CaptureSVNHeadRevision(original_url))
[email protected]fb2b8eb2009-04-23 21:03:421463 else:
1464 url_components = original_url.split("@")
1465 if revision_overrides.has_key(name):
1466 return (url_components[0], int(revision_overrides[name]))
1467 else:
1468 return (url_components[0], int(url_components[1]))
1469
1470 # Run on the base solutions first.
1471 for solution in solutions:
1472 name = solution["name"]
1473 if name in entries:
1474 raise Error("solution %s specified more than once" % name)
1475 (url, rev) = GetURLAndRev(name, solution["url"])
1476 entries[name] = "%s@%d" % (url, rev)
1477 # TODO(aharper): SVN/SCMWrapper cleanup (non-local commandset)
1478 entries_deps_content[name] = CaptureSVN(
[email protected]fb2b8eb2009-04-23 21:03:421479 ["cat",
1480 "%s/%s@%d" % (url,
1481 self._options.deps_file,
1482 rev)],
1483 os.getcwd())
1484
1485 # Process the dependencies next (sort alphanumerically to ensure that
1486 # containing directories get populated first and for readability)
1487 deps = self._ParseAllDeps(entries, entries_deps_content)
1488 deps_to_process = deps.keys()
1489 deps_to_process.sort()
1490
1491 # First pass for direct dependencies.
1492 for d in deps_to_process:
1493 if type(deps[d]) == str:
1494 (url, rev) = GetURLAndRev(d, deps[d])
1495 entries[d] = "%s@%d" % (url, rev)
1496
1497 # Second pass for inherited deps (via the From keyword)
1498 for d in deps_to_process:
1499 if type(deps[d]) != str:
1500 deps_parent_url = entries[deps[d].module_name]
1501 if deps_parent_url.find("@") < 0:
1502 raise Error("From %s missing revisioned url" % deps[d].module_name)
1503 deps_parent_url_components = deps_parent_url.split("@")
1504 # TODO(aharper): SVN/SCMWrapper cleanup (non-local commandset)
1505 deps_parent_content = CaptureSVN(
[email protected]fb2b8eb2009-04-23 21:03:421506 ["cat",
1507 "%s/%s@%s" % (deps_parent_url_components[0],
1508 self._options.deps_file,
1509 deps_parent_url_components[1])],
1510 os.getcwd())
1511 sub_deps = self._ParseSolutionDeps(
1512 deps[d].module_name,
1513 FileRead(os.path.join(self._root_dir,
1514 deps[d].module_name,
1515 self._options.deps_file)),
1516 {})
1517 (url, rev) = GetURLAndRev(d, sub_deps[d])
1518 entries[d] = "%s@%d" % (url, rev)
1519
[email protected]df7a3132009-05-12 17:49:491520 print(";".join(["%s,%s" % (x, entries[x]) for x in sorted(entries.keys())]))
[email protected]fb2b8eb2009-04-23 21:03:421521
1522
1523## gclient commands.
1524
1525
1526def DoCleanup(options, args):
1527 """Handle the cleanup subcommand.
1528
1529 Raises:
1530 Error: if client isn't configured properly.
1531 """
[email protected]2806acc2009-05-15 12:33:341532 client = GClient.LoadCurrentConfig(options)
[email protected]fb2b8eb2009-04-23 21:03:421533 if not client:
1534 raise Error("client not configured; see 'gclient config'")
1535 if options.verbose:
1536 # Print out the .gclient file. This is longer than if we just printed the
1537 # client dict, but more legible, and it might contain helpful comments.
[email protected]df7a3132009-05-12 17:49:491538 print(client.ConfigContent())
[email protected]fb2b8eb2009-04-23 21:03:421539 options.verbose = True
1540 return client.RunOnDeps('cleanup', args)
1541
1542
1543def DoConfig(options, args):
1544 """Handle the config subcommand.
1545
1546 Args:
1547 options: If options.spec set, a string providing contents of config file.
1548 args: The command line args. If spec is not set,
1549 then args[0] is a string URL to get for config file.
1550
1551 Raises:
1552 Error: on usage error
1553 """
1554 if len(args) < 1 and not options.spec:
1555 raise Error("required argument missing; see 'gclient help config'")
[email protected]0329e672009-05-13 18:41:041556 if os.path.exists(options.config_filename):
[email protected]fb2b8eb2009-04-23 21:03:421557 raise Error("%s file already exists in the current directory" %
1558 options.config_filename)
[email protected]2806acc2009-05-15 12:33:341559 client = GClient('.', options)
[email protected]fb2b8eb2009-04-23 21:03:421560 if options.spec:
1561 client.SetConfig(options.spec)
1562 else:
1563 # TODO(darin): it would be nice to be able to specify an alternate relpath
1564 # for the given URL.
[email protected]1ab7ffc2009-06-03 17:21:371565 base_url = args[0].rstrip('/')
1566 name = base_url.split("/")[-1]
[email protected]fb2b8eb2009-04-23 21:03:421567 safesync_url = ""
1568 if len(args) > 1:
1569 safesync_url = args[1]
1570 client.SetDefaultConfig(name, base_url, safesync_url)
1571 client.SaveConfig()
1572
1573
[email protected]644aa0c2009-07-17 20:20:411574def DoExport(options, args):
1575 """Handle the export subcommand.
[email protected]71b40682009-07-31 23:40:091576
[email protected]644aa0c2009-07-17 20:20:411577 Raises:
1578 Error: on usage error
1579 """
1580 if len(args) != 1:
1581 raise Error("Need directory name")
1582 client = GClient.LoadCurrentConfig(options)
1583
1584 if not client:
1585 raise Error("client not configured; see 'gclient config'")
1586
1587 if options.verbose:
1588 # Print out the .gclient file. This is longer than if we just printed the
1589 # client dict, but more legible, and it might contain helpful comments.
1590 print(client.ConfigContent())
1591 return client.RunOnDeps('export', args)
1592
[email protected]fb2b8eb2009-04-23 21:03:421593def DoHelp(options, args):
1594 """Handle the help subcommand giving help for another subcommand.
1595
1596 Raises:
1597 Error: if the command is unknown.
1598 """
1599 if len(args) == 1 and args[0] in COMMAND_USAGE_TEXT:
[email protected]df7a3132009-05-12 17:49:491600 print(COMMAND_USAGE_TEXT[args[0]])
[email protected]fb2b8eb2009-04-23 21:03:421601 else:
1602 raise Error("unknown subcommand '%s'; see 'gclient help'" % args[0])
1603
1604
1605def DoStatus(options, args):
1606 """Handle the status subcommand.
1607
1608 Raises:
1609 Error: if client isn't configured properly.
1610 """
[email protected]2806acc2009-05-15 12:33:341611 client = GClient.LoadCurrentConfig(options)
[email protected]fb2b8eb2009-04-23 21:03:421612 if not client:
1613 raise Error("client not configured; see 'gclient config'")
1614 if options.verbose:
1615 # Print out the .gclient file. This is longer than if we just printed the
1616 # client dict, but more legible, and it might contain helpful comments.
[email protected]df7a3132009-05-12 17:49:491617 print(client.ConfigContent())
[email protected]fb2b8eb2009-04-23 21:03:421618 options.verbose = True
1619 return client.RunOnDeps('status', args)
1620
1621
1622def DoUpdate(options, args):
1623 """Handle the update and sync subcommands.
1624
1625 Raises:
1626 Error: if client isn't configured properly.
1627 """
[email protected]2806acc2009-05-15 12:33:341628 client = GClient.LoadCurrentConfig(options)
[email protected]fb2b8eb2009-04-23 21:03:421629
1630 if not client:
1631 raise Error("client not configured; see 'gclient config'")
1632
1633 if not options.head:
1634 solutions = client.GetVar('solutions')
1635 if solutions:
1636 for s in solutions:
1637 if s.get('safesync_url', ''):
1638 # rip through revisions and make sure we're not over-riding
1639 # something that was explicitly passed
1640 has_key = False
1641 for r in options.revisions:
1642 if r.split('@')[0] == s['name']:
1643 has_key = True
1644 break
1645
1646 if not has_key:
1647 handle = urllib.urlopen(s['safesync_url'])
1648 rev = handle.read().strip()
1649 handle.close()
1650 if len(rev):
1651 options.revisions.append(s['name']+'@'+rev)
1652
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 return client.RunOnDeps('update', args)
1658
1659
1660def DoDiff(options, args):
1661 """Handle the diff subcommand.
1662
1663 Raises:
1664 Error: if client isn't configured properly.
1665 """
[email protected]2806acc2009-05-15 12:33:341666 client = GClient.LoadCurrentConfig(options)
[email protected]fb2b8eb2009-04-23 21:03:421667 if not client:
1668 raise Error("client not configured; see 'gclient config'")
1669 if options.verbose:
1670 # Print out the .gclient file. This is longer than if we just printed the
1671 # client dict, but more legible, and it might contain helpful comments.
[email protected]df7a3132009-05-12 17:49:491672 print(client.ConfigContent())
[email protected]fb2b8eb2009-04-23 21:03:421673 options.verbose = True
1674 return client.RunOnDeps('diff', args)
1675
1676
1677def DoRevert(options, args):
1678 """Handle the revert subcommand.
1679
1680 Raises:
1681 Error: if client isn't configured properly.
1682 """
[email protected]2806acc2009-05-15 12:33:341683 client = GClient.LoadCurrentConfig(options)
[email protected]fb2b8eb2009-04-23 21:03:421684 if not client:
1685 raise Error("client not configured; see 'gclient config'")
1686 return client.RunOnDeps('revert', args)
1687
1688
1689def DoRunHooks(options, args):
1690 """Handle the runhooks 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 if options.verbose:
1699 # Print out the .gclient file. This is longer than if we just printed the
1700 # client dict, but more legible, and it might contain helpful comments.
[email protected]df7a3132009-05-12 17:49:491701 print(client.ConfigContent())
[email protected]fb2b8eb2009-04-23 21:03:421702 return client.RunOnDeps('runhooks', args)
1703
1704
1705def DoRevInfo(options, args):
1706 """Handle the revinfo subcommand.
1707
1708 Raises:
1709 Error: if client isn't configured properly.
1710 """
[email protected]2806acc2009-05-15 12:33:341711 client = GClient.LoadCurrentConfig(options)
[email protected]fb2b8eb2009-04-23 21:03:421712 if not client:
1713 raise Error("client not configured; see 'gclient config'")
1714 client.PrintRevInfo()
1715
1716
1717gclient_command_map = {
1718 "cleanup": DoCleanup,
1719 "config": DoConfig,
1720 "diff": DoDiff,
[email protected]644aa0c2009-07-17 20:20:411721 "export": DoExport,
[email protected]fb2b8eb2009-04-23 21:03:421722 "help": DoHelp,
1723 "status": DoStatus,
1724 "sync": DoUpdate,
1725 "update": DoUpdate,
1726 "revert": DoRevert,
1727 "runhooks": DoRunHooks,
1728 "revinfo" : DoRevInfo,
1729}
1730
1731
1732def DispatchCommand(command, options, args, command_map=None):
1733 """Dispatches the appropriate subcommand based on command line arguments."""
1734 if command_map is None:
1735 command_map = gclient_command_map
1736
1737 if command in command_map:
1738 return command_map[command](options, args)
1739 else:
1740 raise Error("unknown subcommand '%s'; see 'gclient help'" % command)
1741
1742
1743def Main(argv):
1744 """Parse command line arguments and dispatch command."""
1745
1746 option_parser = optparse.OptionParser(usage=DEFAULT_USAGE_TEXT,
1747 version=__version__)
1748 option_parser.disable_interspersed_args()
1749 option_parser.add_option("", "--force", action="store_true", default=False,
1750 help=("(update/sync only) force update even "
1751 "for modules which haven't changed"))
[email protected]67820ef2009-07-27 17:23:001752 option_parser.add_option("", "--nohooks", action="store_true", default=False,
1753 help=("(update/sync/revert only) prevent the hooks from "
1754 "running"))
[email protected]fb2b8eb2009-04-23 21:03:421755 option_parser.add_option("", "--revision", action="append", dest="revisions",
1756 metavar="REV", default=[],
1757 help=("(update/sync only) sync to a specific "
1758 "revision, can be used multiple times for "
1759 "each solution, e.g. --revision=src@123, "
1760 "--revision=internal@32"))
1761 option_parser.add_option("", "--deps", default=None, dest="deps_os",
1762 metavar="OS_LIST",
1763 help=("(update/sync only) sync deps for the "
1764 "specified (comma-separated) platform(s); "
1765 "'all' will sync all platforms"))
1766 option_parser.add_option("", "--spec", default=None,
1767 help=("(config only) create a gclient file "
1768 "containing the provided string"))
1769 option_parser.add_option("", "--verbose", action="store_true", default=False,
1770 help="produce additional output for diagnostics")
1771 option_parser.add_option("", "--manually_grab_svn_rev", action="store_true",
1772 default=False,
1773 help="Skip svn up whenever possible by requesting "
1774 "actual HEAD revision from the repository")
1775 option_parser.add_option("", "--head", action="store_true", default=False,
1776 help=("skips any safesync_urls specified in "
1777 "configured solutions"))
[email protected]cdcee802009-06-23 15:30:421778 option_parser.add_option("", "--delete_unversioned_trees",
1779 action="store_true", default=False,
1780 help=("on update, delete any unexpected "
1781 "unversioned trees that are in the checkout"))
[email protected]fb2b8eb2009-04-23 21:03:421782
1783 if len(argv) < 2:
1784 # Users don't need to be told to use the 'help' command.
1785 option_parser.print_help()
1786 return 1
1787 # Add manual support for --version as first argument.
1788 if argv[1] == '--version':
1789 option_parser.print_version()
1790 return 0
1791
1792 # Add manual support for --help as first argument.
1793 if argv[1] == '--help':
1794 argv[1] = 'help'
1795
1796 command = argv[1]
1797 options, args = option_parser.parse_args(argv[2:])
1798
1799 if len(argv) < 3 and command == "help":
1800 option_parser.print_help()
1801 return 0
1802
1803 # Files used for configuration and state saving.
1804 options.config_filename = os.environ.get("GCLIENT_FILE", ".gclient")
1805 options.entries_filename = ".gclient_entries"
1806 options.deps_file = "DEPS"
1807
[email protected]fb2b8eb2009-04-23 21:03:421808 options.platform = sys.platform
1809 return DispatchCommand(command, options, args)
1810
1811
1812if "__main__" == __name__:
1813 try:
1814 result = Main(sys.argv)
1815 except Error, e:
[email protected]df7a3132009-05-12 17:49:491816 print >> sys.stderr, "Error: %s" % str(e)
[email protected]fb2b8eb2009-04-23 21:03:421817 result = 1
1818 sys.exit(result)
1819
1820# vim: ts=2:sw=2:tw=80:et: