blob: 694435cf5397cd21e4dda602002305dec73f19dc [file] [log] [blame]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001#!/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
40 working copy as a result of a "sync"/"update" or "revert" operation. Hooks
41 can also be run based on what files have been modified in the working copy
42 with the "runhooks" operation. If any of these operation are run with
43 --force, all known hooks will run regardless of the state of the working
44 copy.
45
46 Each item in a "hooks" list is a dict, containing these two keys:
47 "pattern" The associated value is a string containing a regular
48 expression. When a file whose pathname matches the expression
49 is checked out, updated, or reverted, the hook's "action" will
50 run.
51 "action" A list describing a command to run along with its arguments, if
52 any. An action command will run at most one time per gclient
53 invocation, regardless of how many files matched the pattern.
54 The action is executed in the same directory as the .gclient
55 file. If the first item in the list is the string "python",
56 the current Python interpreter (sys.executable) will be used
57 to run the command.
58
59 Example:
60 hooks = [
61 { "pattern": "\\.(gif|jpe?g|pr0n|png)$",
62 "action": ["python", "image_indexer.py", "--all"]},
63 ]
64"""
65
66__author__ = "darinf@gmail.com (Darin Fisher)"
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +000067__version__ = "0.3.2"
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000068
69import errno
70import optparse
71import os
72import re
73import stat
74import subprocess
75import sys
76import time
77import urlparse
78import xml.dom.minidom
79import urllib
80
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000081
82SVN_COMMAND = "svn"
83
84
85# default help text
86DEFAULT_USAGE_TEXT = (
87"""usage: %prog <subcommand> [options] [--] [svn options/args...]
88a wrapper for managing a set of client modules in svn.
89Version """ + __version__ + """
90
91subcommands:
92 cleanup
93 config
94 diff
95 revert
96 status
97 sync
98 update
99 runhooks
100 revinfo
101
102Options and extra arguments can be passed to invoked svn commands by
103appending them to the command line. Note that if the first such
104appended option starts with a dash (-) then the options must be
105preceded by -- to distinguish them from gclient options.
106
107For additional help on a subcommand or examples of usage, try
108 %prog help <subcommand>
109 %prog help files
110""")
111
112GENERIC_UPDATE_USAGE_TEXT = (
113 """Perform a checkout/update of the modules specified by the gclient
114configuration; see 'help config'. Unless --revision is specified,
115then the latest revision of the root solutions is checked out, with
116dependent submodule versions updated according to DEPS files.
117If --revision is specified, then the given revision is used in place
118of the latest, either for a single solution or for all solutions.
119Unless the --force option is provided, solutions and modules whose
120local revision matches the one to update (i.e., they have not changed
121in the repository) are *not* modified.
122This a synonym for 'gclient %(alias)s'
123
124usage: gclient %(cmd)s [options] [--] [svn update options/args]
125
126Valid options:
127 --force : force update even for unchanged modules
128 --revision REV : update/checkout all solutions with specified revision
129 --revision SOLUTION@REV : update given solution to specified revision
130 --deps PLATFORM(S) : sync deps for the given platform(s), or 'all'
131 --verbose : output additional diagnostics
132
133Examples:
134 gclient %(cmd)s
135 update files from SVN according to current configuration,
136 *for modules which have changed since last update or sync*
137 gclient %(cmd)s --force
138 update files from SVN according to current configuration, for
139 all modules (useful for recovering files deleted from local copy)
140""")
141
142COMMAND_USAGE_TEXT = {
143 "cleanup":
144 """Clean up all working copies, using 'svn cleanup' for each module.
145Additional options and args may be passed to 'svn cleanup'.
146
147usage: cleanup [options] [--] [svn cleanup args/options]
148
149Valid options:
150 --verbose : output additional diagnostics
151""",
152 "config": """Create a .gclient file in the current directory; this
153specifies the configuration for further commands. After update/sync,
154top-level DEPS files in each module are read to determine dependent
155modules to operate on as well. If optional [url] parameter is
156provided, then configuration is read from a specified Subversion server
157URL. Otherwise, a --spec option must be provided.
158
159usage: config [option | url] [safesync url]
160
161Valid options:
162 --spec=GCLIENT_SPEC : contents of .gclient are read from string parameter.
163 *Note that due to Cygwin/Python brokenness, it
164 probably can't contain any newlines.*
165
166Examples:
167 gclient config https://gclient.googlecode.com/svn/trunk/gclient
168 configure a new client to check out gclient.py tool sources
169 gclient config --spec='solutions=[{"name":"gclient","""
170 '"url":"https://gclient.googlecode.com/svn/trunk/gclient",'
171 '"custom_deps":{}}]',
172 "diff": """Display the differences between two revisions of modules.
173(Does 'svn diff' for each checked out module and dependences.)
174Additional args and options to 'svn diff' can be passed after
175gclient options.
176
177usage: diff [options] [--] [svn args/options]
178
179Valid options:
180 --verbose : output additional diagnostics
181
182Examples:
183 gclient diff
184 simple 'svn diff' for configured client and dependences
185 gclient diff -- -x -b
186 use 'svn diff -x -b' to suppress whitespace-only differences
187 gclient diff -- -r HEAD -x -b
188 diff versus the latest version of each module
189""",
190 "revert":
191 """Revert every file in every managed directory in the client view.
192
193usage: revert
194""",
195 "status":
196 """Show the status of client and dependent modules, using 'svn diff'
197for each module. Additional options and args may be passed to 'svn diff'.
198
199usage: status [options] [--] [svn diff args/options]
200
201Valid options:
202 --verbose : output additional diagnostics
203""",
204 "sync": GENERIC_UPDATE_USAGE_TEXT % {"cmd": "sync", "alias": "update"},
205 "update": GENERIC_UPDATE_USAGE_TEXT % {"cmd": "update", "alias": "sync"},
206 "help": """Describe the usage of this program or its subcommands.
207
208usage: help [options] [subcommand]
209
210Valid options:
211 --verbose : output additional diagnostics
212""",
213 "runhooks":
214 """Runs hooks for files that have been modified in the local working copy,
215according to 'svn status'.
216
217usage: runhooks [options]
218
219Valid options:
220 --force : runs all known hooks, regardless of the working
221 copy status
222 --verbose : output additional diagnostics
223""",
224 "revinfo":
225 """Outputs source path, server URL and revision information for every
226dependency in all solutions (no local checkout required).
227
228usage: revinfo [options]
229""",
230}
231
232# parameterized by (solution_name, solution_url, safesync_url)
233DEFAULT_CLIENT_FILE_TEXT = (
234 """
235# An element of this array (a \"solution\") describes a repository directory
236# that will be checked out into your working copy. Each solution may
237# optionally define additional dependencies (via its DEPS file) to be
238# checked out alongside the solution's directory. A solution may also
239# specify custom dependencies (via the \"custom_deps\" property) that
240# override or augment the dependencies specified by the DEPS file.
241# If a \"safesync_url\" is specified, it is assumed to reference the location of
242# a text file which contains nothing but the last known good SCM revision to
243# sync against. It is fetched if specified and used unless --head is passed
244solutions = [
245 { \"name\" : \"%s\",
246 \"url\" : \"%s\",
247 \"custom_deps\" : {
248 # To use the trunk of a component instead of what's in DEPS:
249 #\"component\": \"https://svnserver/component/trunk/\",
250 # To exclude a component from your working copy:
251 #\"data/really_large_component\": None,
252 },
253 \"safesync_url\": \"%s\"
254 }
255]
256""")
257
258
259## Generic utils
260
maruel@chromium.orge105d8d2009-04-30 17:58:25 +0000261def ParseXML(output):
262 try:
263 return xml.dom.minidom.parseString(output)
264 except xml.parsers.expat.ExpatError:
265 return None
266
267
maruel@chromium.org483b0082009-05-07 02:57:14 +0000268def GetNamedNodeText(node, node_name):
269 child_nodes = node.getElementsByTagName(node_name)
270 if not child_nodes:
271 return None
272 assert len(child_nodes) == 1 and child_nodes[0].childNodes.length == 1
273 return child_nodes[0].firstChild.nodeValue
274
275
276def GetNodeNamedAttributeText(node, node_name, attribute_name):
277 child_nodes = node.getElementsByTagName(node_name)
278 if not child_nodes:
279 return None
280 assert len(child_nodes) == 1
281 return child_nodes[0].getAttribute(attribute_name)
282
283
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000284class Error(Exception):
285 """gclient exception class."""
286 pass
287
288class PrintableObject(object):
289 def __str__(self):
290 output = ''
291 for i in dir(self):
292 if i.startswith('__'):
293 continue
294 output += '%s = %s\n' % (i, str(getattr(self, i, '')))
295 return output
296
297
298def FileRead(filename):
299 content = None
300 f = open(filename, "rU")
301 try:
302 content = f.read()
303 finally:
304 f.close()
305 return content
306
307
308def FileWrite(filename, content):
309 f = open(filename, "w")
310 try:
311 f.write(content)
312 finally:
313 f.close()
314
315
316def RemoveDirectory(*path):
317 """Recursively removes a directory, even if it's marked read-only.
318
319 Remove the directory located at *path, if it exists.
320
321 shutil.rmtree() doesn't work on Windows if any of the files or directories
322 are read-only, which svn repositories and some .svn files are. We need to
323 be able to force the files to be writable (i.e., deletable) as we traverse
324 the tree.
325
326 Even with all this, Windows still sometimes fails to delete a file, citing
327 a permission error (maybe something to do with antivirus scans or disk
328 indexing). The best suggestion any of the user forums had was to wait a
329 bit and try again, so we do that too. It's hand-waving, but sometimes it
330 works. :/
331
332 On POSIX systems, things are a little bit simpler. The modes of the files
333 to be deleted doesn't matter, only the modes of the directories containing
334 them are significant. As the directory tree is traversed, each directory
335 has its mode set appropriately before descending into it. This should
336 result in the entire tree being removed, with the possible exception of
337 *path itself, because nothing attempts to change the mode of its parent.
338 Doing so would be hazardous, as it's not a directory slated for removal.
339 In the ordinary case, this is not a problem: for our purposes, the user
340 will never lack write permission on *path's parent.
341 """
342 file_path = os.path.join(*path)
343 if not os.path.exists(file_path):
344 return
345
346 if os.path.islink(file_path) or not os.path.isdir(file_path):
347 raise Error("RemoveDirectory asked to remove non-directory %s" % file_path)
348
349 has_win32api = False
350 if sys.platform == 'win32':
351 has_win32api = True
352 # Some people don't have the APIs installed. In that case we'll do without.
353 try:
354 win32api = __import__('win32api')
355 win32con = __import__('win32con')
356 except ImportError:
357 has_win32api = False
358 else:
359 # On POSIX systems, we need the x-bit set on the directory to access it,
360 # the r-bit to see its contents, and the w-bit to remove files from it.
361 # The actual modes of the files within the directory is irrelevant.
362 os.chmod(file_path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
363 for fn in os.listdir(file_path):
364 fullpath = os.path.join(file_path, fn)
365
366 # If fullpath is a symbolic link that points to a directory, isdir will
367 # be True, but we don't want to descend into that as a directory, we just
368 # want to remove the link. Check islink and treat links as ordinary files
369 # would be treated regardless of what they reference.
370 if os.path.islink(fullpath) or not os.path.isdir(fullpath):
371 if sys.platform == 'win32':
372 os.chmod(fullpath, stat.S_IWRITE)
373 if has_win32api:
374 win32api.SetFileAttributes(fullpath, win32con.FILE_ATTRIBUTE_NORMAL)
375 try:
376 os.remove(fullpath)
377 except OSError, e:
378 if e.errno != errno.EACCES or sys.platform != 'win32':
379 raise
380 print 'Failed to delete %s: trying again' % fullpath
381 time.sleep(0.1)
382 os.remove(fullpath)
383 else:
384 RemoveDirectory(fullpath)
385
386 if sys.platform == 'win32':
387 os.chmod(file_path, stat.S_IWRITE)
388 if has_win32api:
389 win32api.SetFileAttributes(file_path, win32con.FILE_ATTRIBUTE_NORMAL)
390 try:
391 os.rmdir(file_path)
392 except OSError, e:
393 if e.errno != errno.EACCES or sys.platform != 'win32':
394 raise
395 print 'Failed to remove %s: trying again' % file_path
396 time.sleep(0.1)
397 os.rmdir(file_path)
398
399
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000400def SubprocessCall(command, in_directory, fail_status=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000401 """Runs command, a list, in directory in_directory.
402
403 This function wraps SubprocessCallAndCapture, but does not perform the
404 capturing functions. See that function for a more complete usage
405 description.
406 """
407 # Call subprocess and capture nothing:
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000408 SubprocessCallAndCapture(command, in_directory, fail_status)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000409
410
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000411def SubprocessCallAndCapture(command, in_directory, fail_status=None,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000412 pattern=None, capture_list=None):
413 """Runs command, a list, in directory in_directory.
414
415 A message indicating what is being done, as well as the command's stdout,
416 is printed to out.
417
418 If a pattern is specified, any line in the output matching pattern will have
419 its first match group appended to capture_list.
420
421 If the command fails, as indicated by a nonzero exit status, gclient will
422 exit with an exit status of fail_status. If fail_status is None (the
423 default), gclient will raise an Error exception.
424 """
425
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000426 print("\n________ running \'%s\' in \'%s\'"
427 % (' '.join(command), in_directory))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000428
429 # *Sigh*: Windows needs shell=True, or else it won't search %PATH% for the
430 # executable, but shell=True makes subprocess on Linux fail when it's called
431 # with a list because it only tries to execute the first item in the list.
432 kid = subprocess.Popen(command, bufsize=0, cwd=in_directory,
433 shell=(sys.platform == 'win32'), stdout=subprocess.PIPE)
434
435 if pattern:
436 compiled_pattern = re.compile(pattern)
437
438 # Also, we need to forward stdout to prevent weird re-ordering of output.
439 # This has to be done on a per byte basis to make sure it is not buffered:
440 # normally buffering is done for each line, but if svn requests input, no
441 # end-of-line character is output after the prompt and it would not show up.
442 in_byte = kid.stdout.read(1)
443 in_line = ""
444 while in_byte:
445 if in_byte != "\r":
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000446 sys.stdout.write(in_byte)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000447 in_line += in_byte
448 if in_byte == "\n" and pattern:
449 match = compiled_pattern.search(in_line[:-1])
450 if match:
451 capture_list.append(match.group(1))
452 in_line = ""
453 in_byte = kid.stdout.read(1)
454 rv = kid.wait()
455
456 if rv:
457 msg = "failed to run command: %s" % " ".join(command)
458
459 if fail_status != None:
460 print >>sys.stderr, msg
461 sys.exit(fail_status)
462
463 raise Error(msg)
464
465
466def IsUsingGit(root, paths):
467 """Returns True if we're using git to manage any of our checkouts.
468 |entries| is a list of paths to check."""
469 for path in paths:
470 if os.path.exists(os.path.join(root, path, '.git')):
471 return True
472 return False
473
474# -----------------------------------------------------------------------------
475# SVN utils:
476
477
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000478def RunSVN(args, in_directory):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000479 """Runs svn, sending output to stdout.
480
481 Args:
482 args: A sequence of command line parameters to be passed to svn.
483 in_directory: The directory where svn is to be run.
484
485 Raises:
486 Error: An error occurred while running the svn command.
487 """
488 c = [SVN_COMMAND]
489 c.extend(args)
490
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000491 SubprocessCall(c, in_directory)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000492
493
maruel@chromium.org5c3a2ff2009-05-12 19:28:55 +0000494def CaptureSVN(args, in_directory=None, print_error=True):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000495 """Runs svn, capturing output sent to stdout as a string.
496
497 Args:
498 args: A sequence of command line parameters to be passed to svn.
499 in_directory: The directory where svn is to be run.
500
501 Returns:
502 The output sent to stdout as a string.
503 """
504 c = [SVN_COMMAND]
505 c.extend(args)
506
507 # *Sigh*: Windows needs shell=True, or else it won't search %PATH% for
508 # the svn.exe executable, but shell=True makes subprocess on Linux fail
509 # when it's called with a list because it only tries to execute the
510 # first string ("svn").
maruel@chromium.org5c3a2ff2009-05-12 19:28:55 +0000511 stderr = None
maruel@chromium.org672343d2009-05-20 20:03:25 +0000512 if not print_error:
maruel@chromium.org5c3a2ff2009-05-12 19:28:55 +0000513 stderr = subprocess.PIPE
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000514 return subprocess.Popen(c,
515 cwd=in_directory,
516 shell=(sys.platform == 'win32'),
maruel@chromium.org5c3a2ff2009-05-12 19:28:55 +0000517 stdout=subprocess.PIPE,
518 stderr=stderr).communicate()[0]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000519
520
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000521def RunSVNAndGetFileList(args, in_directory, file_list):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000522 """Runs svn checkout, update, or status, output to stdout.
523
524 The first item in args must be either "checkout", "update", or "status".
525
526 svn's stdout is parsed to collect a list of files checked out or updated.
527 These files are appended to file_list. svn's stdout is also printed to
528 sys.stdout as in RunSVN.
529
530 Args:
531 args: A sequence of command line parameters to be passed to svn.
532 in_directory: The directory where svn is to be run.
533
534 Raises:
535 Error: An error occurred while running the svn command.
536 """
537 command = [SVN_COMMAND]
538 command.extend(args)
539
540 # svn update and svn checkout use the same pattern: the first three columns
541 # are for file status, property status, and lock status. This is followed
542 # by two spaces, and then the path to the file.
543 update_pattern = '^... (.*)$'
544
545 # The first three columns of svn status are the same as for svn update and
546 # svn checkout. The next three columns indicate addition-with-history,
547 # switch, and remote lock status. This is followed by one space, and then
548 # the path to the file.
549 status_pattern = '^...... (.*)$'
550
551 # args[0] must be a supported command. This will blow up if it's something
552 # else, which is good. Note that the patterns are only effective when
553 # these commands are used in their ordinary forms, the patterns are invalid
554 # for "svn status --show-updates", for example.
555 pattern = {
556 'checkout': update_pattern,
557 'status': status_pattern,
558 'update': update_pattern,
559 }[args[0]]
560
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000561 SubprocessCallAndCapture(command,
562 in_directory,
563 pattern=pattern,
564 capture_list=file_list)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000565
566
maruel@chromium.org5c3a2ff2009-05-12 19:28:55 +0000567def CaptureSVNInfo(relpath, in_directory=None, print_error=True):
maruel@chromium.org2dc8a4d2009-05-11 12:41:20 +0000568 """Returns a dictionary from the svn info output for the given file.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000569
570 Args:
571 relpath: The directory where the working copy resides relative to
572 the directory given by in_directory.
573 in_directory: The directory where svn is to be run.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000574 """
maruel@chromium.org5c3a2ff2009-05-12 19:28:55 +0000575 output = CaptureSVN(["info", "--xml", relpath], in_directory, print_error)
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000576 dom = ParseXML(output)
maruel@chromium.org2dc8a4d2009-05-11 12:41:20 +0000577 result = {}
maruel@chromium.org483b0082009-05-07 02:57:14 +0000578 if dom:
maruel@chromium.org2dc8a4d2009-05-11 12:41:20 +0000579 def C(item, f):
580 if item is not None: return f(item)
maruel@chromium.org483b0082009-05-07 02:57:14 +0000581 # /info/entry/
582 # url
583 # reposityory/(root|uuid)
584 # wc-info/(schedule|depth)
585 # commit/(author|date)
586 # str() the results because they may be returned as Unicode, which
587 # interferes with the higher layers matching up things in the deps
588 # dictionary.
maruel@chromium.org2dc8a4d2009-05-11 12:41:20 +0000589 # TODO(maruel): Fix at higher level instead (!)
590 result['Repository Root'] = C(GetNamedNodeText(dom, 'root'), str)
591 result['URL'] = C(GetNamedNodeText(dom, 'url'), str)
592 result['UUID'] = C(GetNamedNodeText(dom, 'uuid'), str)
593 result['Revision'] = C(GetNodeNamedAttributeText(dom, 'entry', 'revision'),
594 int)
595 result['Node Kind'] = C(GetNodeNamedAttributeText(dom, 'entry', 'kind'),
596 str)
597 result['Schedule'] = C(GetNamedNodeText(dom, 'schedule'), str)
598 result['Path'] = C(GetNodeNamedAttributeText(dom, 'entry', 'path'), str)
599 result['Copied From URL'] = C(GetNamedNodeText(dom, 'copy-from-url'), str)
600 result['Copied From Rev'] = C(GetNamedNodeText(dom, 'copy-from-rev'), str)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000601 return result
602
603
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000604def CaptureSVNHeadRevision(url):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000605 """Get the head revision of a SVN repository.
606
607 Returns:
608 Int head revision
609 """
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000610 info = CaptureSVN(["info", "--xml", url], os.getcwd())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000611 dom = xml.dom.minidom.parseString(info)
612 return int(dom.getElementsByTagName('entry')[0].getAttribute('revision'))
613
614
maruel@chromium.org4810a962009-05-12 21:03:34 +0000615def CaptureSVNStatus(files):
616 """Returns the svn 1.5 svn status emulated output.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000617
maruel@chromium.org4810a962009-05-12 21:03:34 +0000618 @files can be a string (one file) or a list of files.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000619
maruel@chromium.org4810a962009-05-12 21:03:34 +0000620 Returns an array of (status, file) tuples."""
621 command = ["status", "--xml"]
622 if not files:
623 pass
624 elif isinstance(files, basestring):
625 command.append(files)
626 else:
627 command.extend(files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000628
maruel@chromium.org4810a962009-05-12 21:03:34 +0000629 status_letter = {
630 None: ' ',
631 '': ' ',
632 'added': 'A',
633 'conflicted': 'C',
634 'deleted': 'D',
635 'external': 'X',
636 'ignored': 'I',
637 'incomplete': '!',
638 'merged': 'G',
639 'missing': '!',
640 'modified': 'M',
641 'none': ' ',
642 'normal': ' ',
643 'obstructed': '~',
644 'replaced': 'R',
645 'unversioned': '?',
646 }
647 dom = ParseXML(CaptureSVN(command))
maruel@chromium.orge105d8d2009-04-30 17:58:25 +0000648 results = []
649 if dom:
650 # /status/target/entry/(wc-status|commit|author|date)
651 for target in dom.getElementsByTagName('target'):
652 base_path = target.getAttribute('path')
653 for entry in target.getElementsByTagName('entry'):
654 file = entry.getAttribute('path')
655 wc_status = entry.getElementsByTagName('wc-status')
656 assert len(wc_status) == 1
657 # Emulate svn 1.5 status ouput...
658 statuses = [' ' for i in range(7)]
659 # Col 0
660 xml_item_status = wc_status[0].getAttribute('item')
maruel@chromium.org4810a962009-05-12 21:03:34 +0000661 if xml_item_status in status_letter:
662 statuses[0] = status_letter[xml_item_status]
maruel@chromium.orge105d8d2009-04-30 17:58:25 +0000663 else:
664 raise Exception('Unknown item status "%s"; please implement me!' %
665 xml_item_status)
666 # Col 1
667 xml_props_status = wc_status[0].getAttribute('props')
668 if xml_props_status == 'modified':
669 statuses[1] = 'M'
670 elif xml_props_status == 'conflicted':
671 statuses[1] = 'C'
672 elif (not xml_props_status or xml_props_status == 'none' or
673 xml_props_status == 'normal'):
674 pass
675 else:
676 raise Exception('Unknown props status "%s"; please implement me!' %
677 xml_props_status)
maruel@chromium.orgedd27d12009-05-01 17:46:56 +0000678 # Col 2
679 if wc_status[0].getAttribute('wc-locked') == 'true':
680 statuses[2] = 'L'
maruel@chromium.orge105d8d2009-04-30 17:58:25 +0000681 # Col 3
682 if wc_status[0].getAttribute('copied') == 'true':
683 statuses[3] = '+'
maruel@chromium.org4810a962009-05-12 21:03:34 +0000684 item = (''.join(statuses), file)
maruel@chromium.orge105d8d2009-04-30 17:58:25 +0000685 results.append(item)
686 return results
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000687
688
689### SCM abstraction layer
690
691
692class SCMWrapper(object):
693 """Add necessary glue between all the supported SCM.
694
695 This is the abstraction layer to bind to different SCM. Since currently only
696 subversion is supported, a lot of subersionism remains. This can be sorted out
697 once another SCM is supported."""
698 def __init__(self, url=None, root_dir=None, relpath=None,
699 scm_name='svn'):
700 # TODO(maruel): Deduce the SCM from the url.
701 self.scm_name = scm_name
702 self.url = url
703 self._root_dir = root_dir
704 if self._root_dir:
maruel@chromium.orge105d8d2009-04-30 17:58:25 +0000705 self._root_dir = self._root_dir.replace('/', os.sep)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000706 self.relpath = relpath
707 if self.relpath:
maruel@chromium.orge105d8d2009-04-30 17:58:25 +0000708 self.relpath = self.relpath.replace('/', os.sep)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000709
710 def FullUrlForRelativeUrl(self, url):
711 # Find the forth '/' and strip from there. A bit hackish.
712 return '/'.join(self.url.split('/')[:4]) + url
713
714 def RunCommand(self, command, options, args, file_list=None):
715 # file_list will have all files that are modified appended to it.
716
717 if file_list == None:
718 file_list = []
719
720 commands = {
721 'cleanup': self.cleanup,
722 'update': self.update,
723 'revert': self.revert,
724 'status': self.status,
725 'diff': self.diff,
726 'runhooks': self.status,
727 }
728
729 if not command in commands:
730 raise Error('Unknown command %s' % command)
731
732 return commands[command](options, args, file_list)
733
734 def cleanup(self, options, args, file_list):
735 """Cleanup working copy."""
736 command = ['cleanup']
737 command.extend(args)
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000738 RunSVN(command, os.path.join(self._root_dir, self.relpath))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000739
740 def diff(self, options, args, file_list):
741 # NOTE: This function does not currently modify file_list.
742 command = ['diff']
743 command.extend(args)
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000744 RunSVN(command, os.path.join(self._root_dir, self.relpath))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000745
746 def update(self, options, args, file_list):
747 """Runs SCM to update or transparently checkout the working copy.
748
749 All updated files will be appended to file_list.
750
751 Raises:
752 Error: if can't get URL for relative path.
753 """
754 # Only update if git is not controlling the directory.
maruel@chromium.org8626ff72009-05-13 02:57:02 +0000755 checkout_path = os.path.join(self._root_dir, self.relpath)
maruel@chromium.org0329e672009-05-13 18:41:04 +0000756 git_path = os.path.join(self._root_dir, self.relpath, '.git')
757 if os.path.exists(git_path):
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000758 print("________ found .git directory; skipping %s" % self.relpath)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000759 return
760
761 if args:
762 raise Error("Unsupported argument(s): %s" % ",".join(args))
763
764 url = self.url
765 components = url.split("@")
766 revision = None
767 forced_revision = False
768 if options.revision:
769 # Override the revision number.
770 url = '%s@%s' % (components[0], str(options.revision))
771 revision = int(options.revision)
772 forced_revision = True
773 elif len(components) == 2:
774 revision = int(components[1])
775 forced_revision = True
776
777 rev_str = ""
778 if revision:
779 rev_str = ' at %d' % revision
780
maruel@chromium.org0329e672009-05-13 18:41:04 +0000781 if not os.path.exists(checkout_path):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000782 # We need to checkout.
maruel@chromium.org8626ff72009-05-13 02:57:02 +0000783 command = ['checkout', url, checkout_path]
maruel@chromium.orgedd27d12009-05-01 17:46:56 +0000784 if revision:
785 command.extend(['--revision', str(revision)])
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000786 RunSVNAndGetFileList(command, self._root_dir, file_list)
maruel@chromium.orgedd27d12009-05-01 17:46:56 +0000787 return
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000788
789 # Get the existing scm url and the revision number of the current checkout.
maruel@chromium.org8626ff72009-05-13 02:57:02 +0000790 from_info = CaptureSVNInfo(os.path.join(checkout_path, '.'), '.')
maruel@chromium.org1998c6d2009-05-15 12:38:12 +0000791 if not from_info:
792 raise Error("Can't update/checkout %r if an unversioned directory is "
793 "present. Delete the directory and try again." %
794 checkout_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000795
796 if options.manually_grab_svn_rev:
797 # Retrieve the current HEAD version because svn is slow at null updates.
798 if not revision:
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000799 from_info_live = CaptureSVNInfo(from_info['URL'], '.')
maruel@chromium.org2dc8a4d2009-05-11 12:41:20 +0000800 revision = int(from_info_live['Revision'])
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000801 rev_str = ' at %d' % revision
802
maruel@chromium.org2dc8a4d2009-05-11 12:41:20 +0000803 if from_info['URL'] != components[0]:
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000804 to_info = CaptureSVNInfo(url, '.')
maruel@chromium.org8626ff72009-05-13 02:57:02 +0000805 can_switch = ((from_info['Repository Root'] != to_info['Repository Root'])
806 and (from_info['UUID'] == to_info['UUID']))
807 if can_switch:
808 print("\n_____ relocating %s to a new checkout" % self.relpath)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000809 # We have different roots, so check if we can switch --relocate.
810 # Subversion only permits this if the repository UUIDs match.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000811 # Perform the switch --relocate, then rewrite the from_url
812 # to reflect where we "are now." (This is the same way that
813 # Subversion itself handles the metadata when switch --relocate
814 # is used.) This makes the checks below for whether we
815 # can update to a revision or have to switch to a different
816 # branch work as expected.
817 # TODO(maruel): TEST ME !
maruel@chromium.org2dc8a4d2009-05-11 12:41:20 +0000818 command = ["switch", "--relocate",
819 from_info['Repository Root'],
820 to_info['Repository Root'],
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000821 self.relpath]
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000822 RunSVN(command, self._root_dir)
maruel@chromium.org2dc8a4d2009-05-11 12:41:20 +0000823 from_info['URL'] = from_info['URL'].replace(
824 from_info['Repository Root'],
825 to_info['Repository Root'])
maruel@chromium.org8626ff72009-05-13 02:57:02 +0000826 else:
827 if CaptureSVNStatus(checkout_path):
828 raise Error("Can't switch the checkout to %s; UUID don't match and "
829 "there is local changes in %s. Delete the directory and "
830 "try again." % (url, checkout_path))
831 # Ok delete it.
832 print("\n_____ switching %s to a new checkout" % self.relpath)
833 RemoveDirectory(checkout_path)
834 # We need to checkout.
835 command = ['checkout', url, checkout_path]
836 if revision:
837 command.extend(['--revision', str(revision)])
838 RunSVNAndGetFileList(command, self._root_dir, file_list)
839 return
840
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000841
842 # If the provided url has a revision number that matches the revision
843 # number of the existing directory, then we don't need to bother updating.
maruel@chromium.org2dc8a4d2009-05-11 12:41:20 +0000844 if not options.force and from_info['Revision'] == revision:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000845 if options.verbose or not forced_revision:
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000846 print("\n_____ %s%s" % (self.relpath, rev_str))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000847 return
848
maruel@chromium.org8626ff72009-05-13 02:57:02 +0000849 command = ["update", checkout_path]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000850 if revision:
851 command.extend(['--revision', str(revision)])
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000852 RunSVNAndGetFileList(command, self._root_dir, file_list)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000853
854 def revert(self, options, args, file_list):
855 """Reverts local modifications. Subversion specific.
856
857 All reverted files will be appended to file_list, even if Subversion
858 doesn't know about them.
859 """
860 path = os.path.join(self._root_dir, self.relpath)
861 if not os.path.isdir(path):
maruel@chromium.orgedd27d12009-05-01 17:46:56 +0000862 # svn revert won't work if the directory doesn't exist. It needs to
863 # checkout instead.
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000864 print("\n_____ %s is missing, synching instead" % self.relpath)
maruel@chromium.orgedd27d12009-05-01 17:46:56 +0000865 # Don't reuse the args.
866 return self.update(options, [], file_list)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000867
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000868 files = CaptureSVNStatus(path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000869 # Batch the command.
870 files_to_revert = []
871 for file in files:
maruel@chromium.org4810a962009-05-12 21:03:34 +0000872 file_path = os.path.join(path, file[1])
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000873 print(file_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000874 # Unversioned file or unexpected unversioned file.
maruel@chromium.org4810a962009-05-12 21:03:34 +0000875 if file[0][0] in ('?', '~'):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000876 # Remove extraneous file. Also remove unexpected unversioned
877 # directories. svn won't touch them but we want to delete these.
878 file_list.append(file_path)
879 try:
880 os.remove(file_path)
881 except EnvironmentError:
882 RemoveDirectory(file_path)
883
maruel@chromium.org4810a962009-05-12 21:03:34 +0000884 if file[0][0] != '?':
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000885 # For any other status, svn revert will work.
886 file_list.append(file_path)
maruel@chromium.org4810a962009-05-12 21:03:34 +0000887 files_to_revert.append(file[1])
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000888
889 # Revert them all at once.
890 if files_to_revert:
891 accumulated_paths = []
892 accumulated_length = 0
893 command = ['revert']
894 for p in files_to_revert:
895 # Some shell have issues with command lines too long.
896 if accumulated_length and accumulated_length + len(p) > 3072:
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000897 RunSVN(command + accumulated_paths,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000898 os.path.join(self._root_dir, self.relpath))
899 accumulated_paths = []
900 accumulated_length = 0
901 else:
902 accumulated_paths.append(p)
903 accumulated_length += len(p)
904 if accumulated_paths:
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000905 RunSVN(command + accumulated_paths,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000906 os.path.join(self._root_dir, self.relpath))
907
908 def status(self, options, args, file_list):
909 """Display status information."""
maruel@chromium.orgedd27d12009-05-01 17:46:56 +0000910 path = os.path.join(self._root_dir, self.relpath)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000911 command = ['status']
912 command.extend(args)
maruel@chromium.orgedd27d12009-05-01 17:46:56 +0000913 if not os.path.isdir(path):
914 # svn status won't work if the directory doesn't exist.
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000915 print("\n________ couldn't run \'%s\' in \'%s\':\nThe directory "
916 "does not exist."
917 % (' '.join(command), path))
maruel@chromium.orgedd27d12009-05-01 17:46:56 +0000918 # There's no file list to retrieve.
919 else:
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000920 RunSVNAndGetFileList(command, path, file_list)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000921
922
923## GClient implementation.
924
925
926class GClient(object):
927 """Object that represent a gclient checkout."""
928
929 supported_commands = [
930 'cleanup', 'diff', 'revert', 'status', 'update', 'runhooks'
931 ]
932
933 def __init__(self, root_dir, options):
934 self._root_dir = root_dir
935 self._options = options
936 self._config_content = None
937 self._config_dict = {}
938 self._deps_hooks = []
939
940 def SetConfig(self, content):
941 self._config_dict = {}
942 self._config_content = content
skylined@chromium.orgdf0032c2009-05-29 10:43:56 +0000943 try:
944 exec(content, self._config_dict)
945 except SyntaxError, e:
946 try:
947 # Try to construct a human readable error message
948 error_message = [
949 'There is a syntax error in your configuration file.',
950 'Line #%s, character %s:' % (e.lineno, e.offset),
951 '"%s"' % re.sub(r'[\r\n]*$', '', e.text) ]
952 except:
953 # Something went wrong, re-raise the original exception
954 raise e
955 else:
956 # Raise a new exception with the human readable message:
957 raise Error('\n'.join(error_message))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000958
959 def SaveConfig(self):
960 FileWrite(os.path.join(self._root_dir, self._options.config_filename),
961 self._config_content)
962
963 def _LoadConfig(self):
964 client_source = FileRead(os.path.join(self._root_dir,
965 self._options.config_filename))
966 self.SetConfig(client_source)
967
968 def ConfigContent(self):
969 return self._config_content
970
971 def GetVar(self, key, default=None):
972 return self._config_dict.get(key, default)
973
974 @staticmethod
975 def LoadCurrentConfig(options, from_dir=None):
976 """Searches for and loads a .gclient file relative to the current working
977 dir.
978
979 Returns:
980 A dict representing the contents of the .gclient file or an empty dict if
981 the .gclient file doesn't exist.
982 """
983 if not from_dir:
984 from_dir = os.curdir
985 path = os.path.realpath(from_dir)
maruel@chromium.org0329e672009-05-13 18:41:04 +0000986 while not os.path.exists(os.path.join(path, options.config_filename)):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000987 next = os.path.split(path)
988 if not next[1]:
989 return None
990 path = next[0]
maruel@chromium.org2806acc2009-05-15 12:33:34 +0000991 client = GClient(path, options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000992 client._LoadConfig()
993 return client
994
995 def SetDefaultConfig(self, solution_name, solution_url, safesync_url):
996 self.SetConfig(DEFAULT_CLIENT_FILE_TEXT % (
997 solution_name, solution_url, safesync_url
998 ))
999
1000 def _SaveEntries(self, entries):
1001 """Creates a .gclient_entries file to record the list of unique checkouts.
1002
1003 The .gclient_entries file lives in the same directory as .gclient.
1004
1005 Args:
1006 entries: A sequence of solution names.
1007 """
1008 text = "entries = [\n"
1009 for entry in entries:
1010 text += " \"%s\",\n" % entry
1011 text += "]\n"
1012 FileWrite(os.path.join(self._root_dir, self._options.entries_filename),
1013 text)
1014
1015 def _ReadEntries(self):
1016 """Read the .gclient_entries file for the given client.
1017
1018 Args:
1019 client: The client for which the entries file should be read.
1020
1021 Returns:
1022 A sequence of solution names, which will be empty if there is the
1023 entries file hasn't been created yet.
1024 """
1025 scope = {}
1026 filename = os.path.join(self._root_dir, self._options.entries_filename)
maruel@chromium.org0329e672009-05-13 18:41:04 +00001027 if not os.path.exists(filename):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001028 return []
1029 exec(FileRead(filename), scope)
1030 return scope["entries"]
1031
1032 class FromImpl:
1033 """Used to implement the From syntax."""
1034
1035 def __init__(self, module_name):
1036 self.module_name = module_name
1037
1038 def __str__(self):
1039 return 'From("%s")' % self.module_name
1040
1041 class _VarImpl:
1042 def __init__(self, custom_vars, local_scope):
1043 self._custom_vars = custom_vars
1044 self._local_scope = local_scope
1045
1046 def Lookup(self, var_name):
1047 """Implements the Var syntax."""
1048 if var_name in self._custom_vars:
1049 return self._custom_vars[var_name]
1050 elif var_name in self._local_scope.get("vars", {}):
1051 return self._local_scope["vars"][var_name]
1052 raise Error("Var is not defined: %s" % var_name)
1053
1054 def _ParseSolutionDeps(self, solution_name, solution_deps_content,
1055 custom_vars):
1056 """Parses the DEPS file for the specified solution.
1057
1058 Args:
1059 solution_name: The name of the solution to query.
1060 solution_deps_content: Content of the DEPS file for the solution
1061 custom_vars: A dict of vars to override any vars defined in the DEPS file.
1062
1063 Returns:
1064 A dict mapping module names (as relative paths) to URLs or an empty
1065 dict if the solution does not have a DEPS file.
1066 """
1067 # Skip empty
1068 if not solution_deps_content:
1069 return {}
1070 # Eval the content
1071 local_scope = {}
1072 var = self._VarImpl(custom_vars, local_scope)
1073 global_scope = {"From": self.FromImpl, "Var": var.Lookup, "deps_os": {}}
1074 exec(solution_deps_content, global_scope, local_scope)
1075 deps = local_scope.get("deps", {})
1076
1077 # load os specific dependencies if defined. these dependencies may
1078 # override or extend the values defined by the 'deps' member.
1079 if "deps_os" in local_scope:
1080 deps_os_choices = {
1081 "win32": "win",
1082 "win": "win",
1083 "cygwin": "win",
1084 "darwin": "mac",
1085 "mac": "mac",
1086 "unix": "unix",
1087 "linux": "unix",
1088 "linux2": "unix",
1089 }
1090
1091 if self._options.deps_os is not None:
1092 deps_to_include = self._options.deps_os.split(",")
1093 if "all" in deps_to_include:
1094 deps_to_include = deps_os_choices.values()
1095 else:
1096 deps_to_include = [deps_os_choices.get(self._options.platform, "unix")]
1097
1098 deps_to_include = set(deps_to_include)
1099 for deps_os_key in deps_to_include:
1100 os_deps = local_scope["deps_os"].get(deps_os_key, {})
1101 if len(deps_to_include) > 1:
1102 # Ignore any overrides when including deps for more than one
1103 # platform, so we collect the broadest set of dependencies available.
1104 # We may end up with the wrong revision of something for our
1105 # platform, but this is the best we can do.
1106 deps.update([x for x in os_deps.items() if not x[0] in deps])
1107 else:
1108 deps.update(os_deps)
1109
1110 if 'hooks' in local_scope:
1111 self._deps_hooks.extend(local_scope['hooks'])
1112
1113 # If use_relative_paths is set in the DEPS file, regenerate
1114 # the dictionary using paths relative to the directory containing
1115 # the DEPS file.
1116 if local_scope.get('use_relative_paths'):
1117 rel_deps = {}
1118 for d, url in deps.items():
1119 # normpath is required to allow DEPS to use .. in their
1120 # dependency local path.
1121 rel_deps[os.path.normpath(os.path.join(solution_name, d))] = url
1122 return rel_deps
1123 else:
1124 return deps
1125
1126 def _ParseAllDeps(self, solution_urls, solution_deps_content):
1127 """Parse the complete list of dependencies for the client.
1128
1129 Args:
1130 solution_urls: A dict mapping module names (as relative paths) to URLs
1131 corresponding to the solutions specified by the client. This parameter
1132 is passed as an optimization.
1133 solution_deps_content: A dict mapping module names to the content
1134 of their DEPS files
1135
1136 Returns:
1137 A dict mapping module names (as relative paths) to URLs corresponding
1138 to the entire set of dependencies to checkout for the given client.
1139
1140 Raises:
1141 Error: If a dependency conflicts with another dependency or of a solution.
1142 """
1143 deps = {}
1144 for solution in self.GetVar("solutions"):
1145 custom_vars = solution.get("custom_vars", {})
1146 solution_deps = self._ParseSolutionDeps(
1147 solution["name"],
1148 solution_deps_content[solution["name"]],
1149 custom_vars)
1150
1151 # If a line is in custom_deps, but not in the solution, we want to append
1152 # this line to the solution.
1153 if "custom_deps" in solution:
1154 for d in solution["custom_deps"]:
1155 if d not in solution_deps:
1156 solution_deps[d] = solution["custom_deps"][d]
1157
1158 for d in solution_deps:
1159 if "custom_deps" in solution and d in solution["custom_deps"]:
1160 # Dependency is overriden.
1161 url = solution["custom_deps"][d]
1162 if url is None:
1163 continue
1164 else:
1165 url = solution_deps[d]
1166 # if we have a From reference dependent on another solution, then
1167 # just skip the From reference. When we pull deps for the solution,
1168 # we will take care of this dependency.
1169 #
1170 # If multiple solutions all have the same From reference, then we
1171 # should only add one to our list of dependencies.
1172 if type(url) != str:
1173 if url.module_name in solution_urls:
1174 # Already parsed.
1175 continue
1176 if d in deps and type(deps[d]) != str:
1177 if url.module_name == deps[d].module_name:
1178 continue
1179 else:
1180 parsed_url = urlparse.urlparse(url)
1181 scheme = parsed_url[0]
1182 if not scheme:
1183 # A relative url. Fetch the real base.
1184 path = parsed_url[2]
1185 if path[0] != "/":
1186 raise Error(
1187 "relative DEPS entry \"%s\" must begin with a slash" % d)
1188 # Create a scm just to query the full url.
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001189 scm = SCMWrapper(solution["url"], self._root_dir, None)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001190 url = scm.FullUrlForRelativeUrl(url)
1191 if d in deps and deps[d] != url:
1192 raise Error(
1193 "Solutions have conflicting versions of dependency \"%s\"" % d)
1194 if d in solution_urls and solution_urls[d] != url:
1195 raise Error(
1196 "Dependency \"%s\" conflicts with specified solution" % d)
1197 # Grab the dependency.
1198 deps[d] = url
1199 return deps
1200
1201 def _RunHookAction(self, hook_dict):
1202 """Runs the action from a single hook.
1203 """
1204 command = hook_dict['action'][:]
1205 if command[0] == 'python':
1206 # If the hook specified "python" as the first item, the action is a
1207 # Python script. Run it by starting a new copy of the same
1208 # interpreter.
1209 command[0] = sys.executable
1210
1211 # Use a discrete exit status code of 2 to indicate that a hook action
1212 # failed. Users of this script may wish to treat hook action failures
1213 # differently from VC failures.
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +00001214 SubprocessCall(command, self._root_dir, fail_status=2)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001215
1216 def _RunHooks(self, command, file_list, is_using_git):
1217 """Evaluates all hooks, running actions as needed.
1218 """
1219 # Hooks only run for these command types.
1220 if not command in ('update', 'revert', 'runhooks'):
1221 return
1222
1223 # Get any hooks from the .gclient file.
1224 hooks = self.GetVar("hooks", [])
1225 # Add any hooks found in DEPS files.
1226 hooks.extend(self._deps_hooks)
1227
1228 # If "--force" was specified, run all hooks regardless of what files have
1229 # changed. If the user is using git, then we don't know what files have
1230 # changed so we always run all hooks.
1231 if self._options.force or is_using_git:
1232 for hook_dict in hooks:
1233 self._RunHookAction(hook_dict)
1234 return
1235
1236 # Run hooks on the basis of whether the files from the gclient operation
1237 # match each hook's pattern.
1238 for hook_dict in hooks:
1239 pattern = re.compile(hook_dict['pattern'])
1240 for file in file_list:
1241 if not pattern.search(file):
1242 continue
1243
1244 self._RunHookAction(hook_dict)
1245
1246 # The hook's action only runs once. Don't bother looking for any
1247 # more matches.
1248 break
1249
1250 def RunOnDeps(self, command, args):
1251 """Runs a command on each dependency in a client and its dependencies.
1252
1253 The module's dependencies are specified in its top-level DEPS files.
1254
1255 Args:
1256 command: The command to use (e.g., 'status' or 'diff')
1257 args: list of str - extra arguments to add to the command line.
1258
1259 Raises:
1260 Error: If the client has conflicting entries.
1261 """
1262 if not command in self.supported_commands:
1263 raise Error("'%s' is an unsupported command" % command)
1264
1265 # Check for revision overrides.
1266 revision_overrides = {}
1267 for revision in self._options.revisions:
1268 if revision.find("@") == -1:
1269 raise Error(
1270 "Specify the full dependency when specifying a revision number.")
1271 revision_elem = revision.split("@")
1272 # Disallow conflicting revs
1273 if revision_overrides.has_key(revision_elem[0]) and \
1274 revision_overrides[revision_elem[0]] != revision_elem[1]:
1275 raise Error(
1276 "Conflicting revision numbers specified.")
1277 revision_overrides[revision_elem[0]] = revision_elem[1]
1278
1279 solutions = self.GetVar("solutions")
1280 if not solutions:
1281 raise Error("No solution specified")
1282
1283 # When running runhooks --force, there's no need to consult the SCM.
1284 # All known hooks are expected to run unconditionally regardless of working
1285 # copy state, so skip the SCM status check.
1286 run_scm = not (command == 'runhooks' and self._options.force)
1287
1288 entries = {}
1289 entries_deps_content = {}
1290 file_list = []
1291 # Run on the base solutions first.
1292 for solution in solutions:
1293 name = solution["name"]
1294 if name in entries:
1295 raise Error("solution %s specified more than once" % name)
1296 url = solution["url"]
1297 entries[name] = url
1298 if run_scm:
1299 self._options.revision = revision_overrides.get(name)
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001300 scm = SCMWrapper(url, self._root_dir, name)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001301 scm.RunCommand(command, self._options, args, file_list)
1302 self._options.revision = None
1303 try:
1304 deps_content = FileRead(os.path.join(self._root_dir, name,
1305 self._options.deps_file))
1306 except IOError, e:
1307 if e.errno != errno.ENOENT:
1308 raise
1309 deps_content = ""
1310 entries_deps_content[name] = deps_content
1311
1312 # Process the dependencies next (sort alphanumerically to ensure that
1313 # containing directories get populated first and for readability)
1314 deps = self._ParseAllDeps(entries, entries_deps_content)
1315 deps_to_process = deps.keys()
1316 deps_to_process.sort()
1317
1318 # First pass for direct dependencies.
1319 for d in deps_to_process:
1320 if type(deps[d]) == str:
1321 url = deps[d]
1322 entries[d] = url
1323 if run_scm:
1324 self._options.revision = revision_overrides.get(d)
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001325 scm = SCMWrapper(url, self._root_dir, d)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001326 scm.RunCommand(command, self._options, args, file_list)
1327 self._options.revision = None
1328
1329 # Second pass for inherited deps (via the From keyword)
1330 for d in deps_to_process:
1331 if type(deps[d]) != str:
1332 sub_deps = self._ParseSolutionDeps(
1333 deps[d].module_name,
1334 FileRead(os.path.join(self._root_dir,
1335 deps[d].module_name,
1336 self._options.deps_file)),
1337 {})
1338 url = sub_deps[d]
1339 entries[d] = url
1340 if run_scm:
1341 self._options.revision = revision_overrides.get(d)
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001342 scm = SCMWrapper(url, self._root_dir, d)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001343 scm.RunCommand(command, self._options, args, file_list)
1344 self._options.revision = None
1345
1346 is_using_git = IsUsingGit(self._root_dir, entries.keys())
1347 self._RunHooks(command, file_list, is_using_git)
1348
1349 if command == 'update':
1350 # notify the user if there is an orphaned entry in their working copy.
1351 # TODO(darin): we should delete this directory manually if it doesn't
1352 # have any changes in it.
1353 prev_entries = self._ReadEntries()
1354 for entry in prev_entries:
1355 e_dir = os.path.join(self._root_dir, entry)
maruel@chromium.org0329e672009-05-13 18:41:04 +00001356 if entry not in entries and os.path.exists(e_dir):
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +00001357 if CaptureSVNStatus(e_dir):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001358 # There are modified files in this entry
1359 entries[entry] = None # Keep warning until removed.
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +00001360 print("\nWARNING: \"%s\" is no longer part of this client. "
1361 "It is recommended that you manually remove it.\n") % entry
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001362 else:
1363 # Delete the entry
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +00001364 print("\n________ deleting \'%s\' " +
1365 "in \'%s\'") % (entry, self._root_dir)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001366 RemoveDirectory(e_dir)
1367 # record the current list of entries for next time
1368 self._SaveEntries(entries)
1369
1370 def PrintRevInfo(self):
1371 """Output revision info mapping for the client and its dependencies. This
1372 allows the capture of a overall "revision" for the source tree that can
1373 be used to reproduce the same tree in the future. The actual output
1374 contains enough information (source paths, svn server urls and revisions)
1375 that it can be used either to generate external svn commands (without
1376 gclient) or as input to gclient's --rev option (with some massaging of
1377 the data).
1378
1379 NOTE: Unlike RunOnDeps this does not require a local checkout and is run
1380 on the Pulse master. It MUST NOT execute hooks.
1381
1382 Raises:
1383 Error: If the client has conflicting entries.
1384 """
1385 # Check for revision overrides.
1386 revision_overrides = {}
1387 for revision in self._options.revisions:
1388 if revision.find("@") < 0:
1389 raise Error(
1390 "Specify the full dependency when specifying a revision number.")
1391 revision_elem = revision.split("@")
1392 # Disallow conflicting revs
1393 if revision_overrides.has_key(revision_elem[0]) and \
1394 revision_overrides[revision_elem[0]] != revision_elem[1]:
1395 raise Error(
1396 "Conflicting revision numbers specified.")
1397 revision_overrides[revision_elem[0]] = revision_elem[1]
1398
1399 solutions = self.GetVar("solutions")
1400 if not solutions:
1401 raise Error("No solution specified")
1402
1403 entries = {}
1404 entries_deps_content = {}
1405
1406 # Inner helper to generate base url and rev tuple (including honoring
1407 # |revision_overrides|)
1408 def GetURLAndRev(name, original_url):
1409 if original_url.find("@") < 0:
1410 if revision_overrides.has_key(name):
1411 return (original_url, int(revision_overrides[name]))
1412 else:
1413 # TODO(aharper): SVN/SCMWrapper cleanup (non-local commandset)
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +00001414 return (original_url, CaptureSVNHeadRevision(original_url))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001415 else:
1416 url_components = original_url.split("@")
1417 if revision_overrides.has_key(name):
1418 return (url_components[0], int(revision_overrides[name]))
1419 else:
1420 return (url_components[0], int(url_components[1]))
1421
1422 # Run on the base solutions first.
1423 for solution in solutions:
1424 name = solution["name"]
1425 if name in entries:
1426 raise Error("solution %s specified more than once" % name)
1427 (url, rev) = GetURLAndRev(name, solution["url"])
1428 entries[name] = "%s@%d" % (url, rev)
1429 # TODO(aharper): SVN/SCMWrapper cleanup (non-local commandset)
1430 entries_deps_content[name] = CaptureSVN(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001431 ["cat",
1432 "%s/%s@%d" % (url,
1433 self._options.deps_file,
1434 rev)],
1435 os.getcwd())
1436
1437 # Process the dependencies next (sort alphanumerically to ensure that
1438 # containing directories get populated first and for readability)
1439 deps = self._ParseAllDeps(entries, entries_deps_content)
1440 deps_to_process = deps.keys()
1441 deps_to_process.sort()
1442
1443 # First pass for direct dependencies.
1444 for d in deps_to_process:
1445 if type(deps[d]) == str:
1446 (url, rev) = GetURLAndRev(d, deps[d])
1447 entries[d] = "%s@%d" % (url, rev)
1448
1449 # Second pass for inherited deps (via the From keyword)
1450 for d in deps_to_process:
1451 if type(deps[d]) != str:
1452 deps_parent_url = entries[deps[d].module_name]
1453 if deps_parent_url.find("@") < 0:
1454 raise Error("From %s missing revisioned url" % deps[d].module_name)
1455 deps_parent_url_components = deps_parent_url.split("@")
1456 # TODO(aharper): SVN/SCMWrapper cleanup (non-local commandset)
1457 deps_parent_content = CaptureSVN(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001458 ["cat",
1459 "%s/%s@%s" % (deps_parent_url_components[0],
1460 self._options.deps_file,
1461 deps_parent_url_components[1])],
1462 os.getcwd())
1463 sub_deps = self._ParseSolutionDeps(
1464 deps[d].module_name,
1465 FileRead(os.path.join(self._root_dir,
1466 deps[d].module_name,
1467 self._options.deps_file)),
1468 {})
1469 (url, rev) = GetURLAndRev(d, sub_deps[d])
1470 entries[d] = "%s@%d" % (url, rev)
1471
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +00001472 print(";".join(["%s,%s" % (x, entries[x]) for x in sorted(entries.keys())]))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001473
1474
1475## gclient commands.
1476
1477
1478def DoCleanup(options, args):
1479 """Handle the cleanup subcommand.
1480
1481 Raises:
1482 Error: if client isn't configured properly.
1483 """
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001484 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001485 if not client:
1486 raise Error("client not configured; see 'gclient config'")
1487 if options.verbose:
1488 # Print out the .gclient file. This is longer than if we just printed the
1489 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +00001490 print(client.ConfigContent())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001491 options.verbose = True
1492 return client.RunOnDeps('cleanup', args)
1493
1494
1495def DoConfig(options, args):
1496 """Handle the config subcommand.
1497
1498 Args:
1499 options: If options.spec set, a string providing contents of config file.
1500 args: The command line args. If spec is not set,
1501 then args[0] is a string URL to get for config file.
1502
1503 Raises:
1504 Error: on usage error
1505 """
1506 if len(args) < 1 and not options.spec:
1507 raise Error("required argument missing; see 'gclient help config'")
maruel@chromium.org0329e672009-05-13 18:41:04 +00001508 if os.path.exists(options.config_filename):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001509 raise Error("%s file already exists in the current directory" %
1510 options.config_filename)
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001511 client = GClient('.', options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001512 if options.spec:
1513 client.SetConfig(options.spec)
1514 else:
1515 # TODO(darin): it would be nice to be able to specify an alternate relpath
1516 # for the given URL.
1517 base_url = args[0]
1518 name = args[0].split("/")[-1]
1519 safesync_url = ""
1520 if len(args) > 1:
1521 safesync_url = args[1]
1522 client.SetDefaultConfig(name, base_url, safesync_url)
1523 client.SaveConfig()
1524
1525
1526def DoHelp(options, args):
1527 """Handle the help subcommand giving help for another subcommand.
1528
1529 Raises:
1530 Error: if the command is unknown.
1531 """
1532 if len(args) == 1 and args[0] in COMMAND_USAGE_TEXT:
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +00001533 print(COMMAND_USAGE_TEXT[args[0]])
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001534 else:
1535 raise Error("unknown subcommand '%s'; see 'gclient help'" % args[0])
1536
1537
1538def DoStatus(options, args):
1539 """Handle the status subcommand.
1540
1541 Raises:
1542 Error: if client isn't configured properly.
1543 """
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001544 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001545 if not client:
1546 raise Error("client not configured; see 'gclient config'")
1547 if options.verbose:
1548 # Print out the .gclient file. This is longer than if we just printed the
1549 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +00001550 print(client.ConfigContent())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001551 options.verbose = True
1552 return client.RunOnDeps('status', args)
1553
1554
1555def DoUpdate(options, args):
1556 """Handle the update and sync subcommands.
1557
1558 Raises:
1559 Error: if client isn't configured properly.
1560 """
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001561 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001562
1563 if not client:
1564 raise Error("client not configured; see 'gclient config'")
1565
1566 if not options.head:
1567 solutions = client.GetVar('solutions')
1568 if solutions:
1569 for s in solutions:
1570 if s.get('safesync_url', ''):
1571 # rip through revisions and make sure we're not over-riding
1572 # something that was explicitly passed
1573 has_key = False
1574 for r in options.revisions:
1575 if r.split('@')[0] == s['name']:
1576 has_key = True
1577 break
1578
1579 if not has_key:
1580 handle = urllib.urlopen(s['safesync_url'])
1581 rev = handle.read().strip()
1582 handle.close()
1583 if len(rev):
1584 options.revisions.append(s['name']+'@'+rev)
1585
1586 if options.verbose:
1587 # Print out the .gclient file. This is longer than if we just printed the
1588 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +00001589 print(client.ConfigContent())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001590 return client.RunOnDeps('update', args)
1591
1592
1593def DoDiff(options, args):
1594 """Handle the diff subcommand.
1595
1596 Raises:
1597 Error: if client isn't configured properly.
1598 """
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001599 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001600 if not client:
1601 raise Error("client not configured; see 'gclient config'")
1602 if options.verbose:
1603 # Print out the .gclient file. This is longer than if we just printed the
1604 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +00001605 print(client.ConfigContent())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001606 options.verbose = True
1607 return client.RunOnDeps('diff', args)
1608
1609
1610def DoRevert(options, args):
1611 """Handle the revert subcommand.
1612
1613 Raises:
1614 Error: if client isn't configured properly.
1615 """
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001616 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001617 if not client:
1618 raise Error("client not configured; see 'gclient config'")
1619 return client.RunOnDeps('revert', args)
1620
1621
1622def DoRunHooks(options, args):
1623 """Handle the runhooks subcommand.
1624
1625 Raises:
1626 Error: if client isn't configured properly.
1627 """
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001628 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001629 if not client:
1630 raise Error("client not configured; see 'gclient config'")
1631 if options.verbose:
1632 # Print out the .gclient file. This is longer than if we just printed the
1633 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +00001634 print(client.ConfigContent())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001635 return client.RunOnDeps('runhooks', args)
1636
1637
1638def DoRevInfo(options, args):
1639 """Handle the revinfo subcommand.
1640
1641 Raises:
1642 Error: if client isn't configured properly.
1643 """
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001644 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001645 if not client:
1646 raise Error("client not configured; see 'gclient config'")
1647 client.PrintRevInfo()
1648
1649
1650gclient_command_map = {
1651 "cleanup": DoCleanup,
1652 "config": DoConfig,
1653 "diff": DoDiff,
1654 "help": DoHelp,
1655 "status": DoStatus,
1656 "sync": DoUpdate,
1657 "update": DoUpdate,
1658 "revert": DoRevert,
1659 "runhooks": DoRunHooks,
1660 "revinfo" : DoRevInfo,
1661}
1662
1663
1664def DispatchCommand(command, options, args, command_map=None):
1665 """Dispatches the appropriate subcommand based on command line arguments."""
1666 if command_map is None:
1667 command_map = gclient_command_map
1668
1669 if command in command_map:
1670 return command_map[command](options, args)
1671 else:
1672 raise Error("unknown subcommand '%s'; see 'gclient help'" % command)
1673
1674
1675def Main(argv):
1676 """Parse command line arguments and dispatch command."""
1677
1678 option_parser = optparse.OptionParser(usage=DEFAULT_USAGE_TEXT,
1679 version=__version__)
1680 option_parser.disable_interspersed_args()
1681 option_parser.add_option("", "--force", action="store_true", default=False,
1682 help=("(update/sync only) force update even "
1683 "for modules which haven't changed"))
1684 option_parser.add_option("", "--revision", action="append", dest="revisions",
1685 metavar="REV", default=[],
1686 help=("(update/sync only) sync to a specific "
1687 "revision, can be used multiple times for "
1688 "each solution, e.g. --revision=src@123, "
1689 "--revision=internal@32"))
1690 option_parser.add_option("", "--deps", default=None, dest="deps_os",
1691 metavar="OS_LIST",
1692 help=("(update/sync only) sync deps for the "
1693 "specified (comma-separated) platform(s); "
1694 "'all' will sync all platforms"))
1695 option_parser.add_option("", "--spec", default=None,
1696 help=("(config only) create a gclient file "
1697 "containing the provided string"))
1698 option_parser.add_option("", "--verbose", action="store_true", default=False,
1699 help="produce additional output for diagnostics")
1700 option_parser.add_option("", "--manually_grab_svn_rev", action="store_true",
1701 default=False,
1702 help="Skip svn up whenever possible by requesting "
1703 "actual HEAD revision from the repository")
1704 option_parser.add_option("", "--head", action="store_true", default=False,
1705 help=("skips any safesync_urls specified in "
1706 "configured solutions"))
1707
1708 if len(argv) < 2:
1709 # Users don't need to be told to use the 'help' command.
1710 option_parser.print_help()
1711 return 1
1712 # Add manual support for --version as first argument.
1713 if argv[1] == '--version':
1714 option_parser.print_version()
1715 return 0
1716
1717 # Add manual support for --help as first argument.
1718 if argv[1] == '--help':
1719 argv[1] = 'help'
1720
1721 command = argv[1]
1722 options, args = option_parser.parse_args(argv[2:])
1723
1724 if len(argv) < 3 and command == "help":
1725 option_parser.print_help()
1726 return 0
1727
1728 # Files used for configuration and state saving.
1729 options.config_filename = os.environ.get("GCLIENT_FILE", ".gclient")
1730 options.entries_filename = ".gclient_entries"
1731 options.deps_file = "DEPS"
1732
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001733 options.platform = sys.platform
1734 return DispatchCommand(command, options, args)
1735
1736
1737if "__main__" == __name__:
1738 try:
1739 result = Main(sys.argv)
1740 except Error, e:
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +00001741 print >> sys.stderr, "Error: %s" % str(e)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001742 result = 1
1743 sys.exit(result)
1744
1745# vim: ts=2:sw=2:tw=80:et: