blob: 9413cd11a79317d5689724ca950b0dd84e6eb498 [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
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +000095 export
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000096 revert
97 status
98 sync
99 update
100 runhooks
101 revinfo
102
103Options and extra arguments can be passed to invoked svn commands by
104appending them to the command line. Note that if the first such
105appended option starts with a dash (-) then the options must be
106preceded by -- to distinguish them from gclient options.
107
108For additional help on a subcommand or examples of usage, try
109 %prog help <subcommand>
110 %prog help files
111""")
112
113GENERIC_UPDATE_USAGE_TEXT = (
114 """Perform a checkout/update of the modules specified by the gclient
115configuration; see 'help config'. Unless --revision is specified,
116then the latest revision of the root solutions is checked out, with
117dependent submodule versions updated according to DEPS files.
118If --revision is specified, then the given revision is used in place
119of the latest, either for a single solution or for all solutions.
120Unless the --force option is provided, solutions and modules whose
121local revision matches the one to update (i.e., they have not changed
122in the repository) are *not* modified.
123This a synonym for 'gclient %(alias)s'
124
125usage: gclient %(cmd)s [options] [--] [svn update options/args]
126
127Valid options:
128 --force : force update even for unchanged modules
129 --revision REV : update/checkout all solutions with specified revision
130 --revision SOLUTION@REV : update given solution to specified revision
131 --deps PLATFORM(S) : sync deps for the given platform(s), or 'all'
132 --verbose : output additional diagnostics
maruel@chromium.orgb8b6b872009-06-30 18:50:56 +0000133 --head : update to latest revision, instead of last good revision
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000134
135Examples:
136 gclient %(cmd)s
137 update files from SVN according to current configuration,
138 *for modules which have changed since last update or sync*
139 gclient %(cmd)s --force
140 update files from SVN according to current configuration, for
141 all modules (useful for recovering files deleted from local copy)
142""")
143
144COMMAND_USAGE_TEXT = {
145 "cleanup":
146 """Clean up all working copies, using 'svn cleanup' for each module.
147Additional options and args may be passed to 'svn cleanup'.
148
149usage: cleanup [options] [--] [svn cleanup args/options]
150
151Valid options:
152 --verbose : output additional diagnostics
153""",
154 "config": """Create a .gclient file in the current directory; this
155specifies the configuration for further commands. After update/sync,
156top-level DEPS files in each module are read to determine dependent
157modules to operate on as well. If optional [url] parameter is
158provided, then configuration is read from a specified Subversion server
159URL. Otherwise, a --spec option must be provided.
160
161usage: config [option | url] [safesync url]
162
163Valid options:
164 --spec=GCLIENT_SPEC : contents of .gclient are read from string parameter.
165 *Note that due to Cygwin/Python brokenness, it
166 probably can't contain any newlines.*
167
168Examples:
169 gclient config https://gclient.googlecode.com/svn/trunk/gclient
170 configure a new client to check out gclient.py tool sources
171 gclient config --spec='solutions=[{"name":"gclient","""
172 '"url":"https://gclient.googlecode.com/svn/trunk/gclient",'
173 '"custom_deps":{}}]',
174 "diff": """Display the differences between two revisions of modules.
175(Does 'svn diff' for each checked out module and dependences.)
176Additional args and options to 'svn diff' can be passed after
177gclient options.
178
179usage: diff [options] [--] [svn args/options]
180
181Valid options:
182 --verbose : output additional diagnostics
183
184Examples:
185 gclient diff
186 simple 'svn diff' for configured client and dependences
187 gclient diff -- -x -b
188 use 'svn diff -x -b' to suppress whitespace-only differences
189 gclient diff -- -r HEAD -x -b
190 diff versus the latest version of each module
191""",
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +0000192 "export":
193 """Wrapper for svn export for all managed directories
194""",
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000195 "revert":
196 """Revert every file in every managed directory in the client view.
197
198usage: revert
199""",
200 "status":
201 """Show the status of client and dependent modules, using 'svn diff'
202for each module. Additional options and args may be passed to 'svn diff'.
203
204usage: status [options] [--] [svn diff args/options]
205
206Valid options:
207 --verbose : output additional diagnostics
208""",
209 "sync": GENERIC_UPDATE_USAGE_TEXT % {"cmd": "sync", "alias": "update"},
210 "update": GENERIC_UPDATE_USAGE_TEXT % {"cmd": "update", "alias": "sync"},
211 "help": """Describe the usage of this program or its subcommands.
212
213usage: help [options] [subcommand]
214
215Valid options:
216 --verbose : output additional diagnostics
217""",
218 "runhooks":
219 """Runs hooks for files that have been modified in the local working copy,
220according to 'svn status'.
221
222usage: runhooks [options]
223
224Valid options:
225 --force : runs all known hooks, regardless of the working
226 copy status
227 --verbose : output additional diagnostics
228""",
229 "revinfo":
230 """Outputs source path, server URL and revision information for every
231dependency in all solutions (no local checkout required).
232
233usage: revinfo [options]
234""",
235}
236
237# parameterized by (solution_name, solution_url, safesync_url)
238DEFAULT_CLIENT_FILE_TEXT = (
239 """
240# An element of this array (a \"solution\") describes a repository directory
241# that will be checked out into your working copy. Each solution may
242# optionally define additional dependencies (via its DEPS file) to be
243# checked out alongside the solution's directory. A solution may also
244# specify custom dependencies (via the \"custom_deps\" property) that
245# override or augment the dependencies specified by the DEPS file.
246# If a \"safesync_url\" is specified, it is assumed to reference the location of
247# a text file which contains nothing but the last known good SCM revision to
248# sync against. It is fetched if specified and used unless --head is passed
249solutions = [
250 { \"name\" : \"%s\",
251 \"url\" : \"%s\",
252 \"custom_deps\" : {
253 # To use the trunk of a component instead of what's in DEPS:
254 #\"component\": \"https://svnserver/component/trunk/\",
255 # To exclude a component from your working copy:
256 #\"data/really_large_component\": None,
257 },
258 \"safesync_url\": \"%s\"
259 }
260]
261""")
262
263
264## Generic utils
265
maruel@chromium.orge105d8d2009-04-30 17:58:25 +0000266def ParseXML(output):
267 try:
268 return xml.dom.minidom.parseString(output)
269 except xml.parsers.expat.ExpatError:
270 return None
271
272
maruel@chromium.org483b0082009-05-07 02:57:14 +0000273def GetNamedNodeText(node, node_name):
274 child_nodes = node.getElementsByTagName(node_name)
275 if not child_nodes:
276 return None
277 assert len(child_nodes) == 1 and child_nodes[0].childNodes.length == 1
278 return child_nodes[0].firstChild.nodeValue
279
280
281def GetNodeNamedAttributeText(node, node_name, attribute_name):
282 child_nodes = node.getElementsByTagName(node_name)
283 if not child_nodes:
284 return None
285 assert len(child_nodes) == 1
286 return child_nodes[0].getAttribute(attribute_name)
287
288
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000289class Error(Exception):
290 """gclient exception class."""
291 pass
292
293class PrintableObject(object):
294 def __str__(self):
295 output = ''
296 for i in dir(self):
297 if i.startswith('__'):
298 continue
299 output += '%s = %s\n' % (i, str(getattr(self, i, '')))
300 return output
301
302
303def FileRead(filename):
304 content = None
305 f = open(filename, "rU")
306 try:
307 content = f.read()
308 finally:
309 f.close()
310 return content
311
312
313def FileWrite(filename, content):
314 f = open(filename, "w")
315 try:
316 f.write(content)
317 finally:
318 f.close()
319
320
321def RemoveDirectory(*path):
322 """Recursively removes a directory, even if it's marked read-only.
323
324 Remove the directory located at *path, if it exists.
325
326 shutil.rmtree() doesn't work on Windows if any of the files or directories
327 are read-only, which svn repositories and some .svn files are. We need to
328 be able to force the files to be writable (i.e., deletable) as we traverse
329 the tree.
330
331 Even with all this, Windows still sometimes fails to delete a file, citing
332 a permission error (maybe something to do with antivirus scans or disk
333 indexing). The best suggestion any of the user forums had was to wait a
334 bit and try again, so we do that too. It's hand-waving, but sometimes it
335 works. :/
336
337 On POSIX systems, things are a little bit simpler. The modes of the files
338 to be deleted doesn't matter, only the modes of the directories containing
339 them are significant. As the directory tree is traversed, each directory
340 has its mode set appropriately before descending into it. This should
341 result in the entire tree being removed, with the possible exception of
342 *path itself, because nothing attempts to change the mode of its parent.
343 Doing so would be hazardous, as it's not a directory slated for removal.
344 In the ordinary case, this is not a problem: for our purposes, the user
345 will never lack write permission on *path's parent.
346 """
347 file_path = os.path.join(*path)
348 if not os.path.exists(file_path):
349 return
350
351 if os.path.islink(file_path) or not os.path.isdir(file_path):
352 raise Error("RemoveDirectory asked to remove non-directory %s" % file_path)
353
354 has_win32api = False
355 if sys.platform == 'win32':
356 has_win32api = True
357 # Some people don't have the APIs installed. In that case we'll do without.
358 try:
359 win32api = __import__('win32api')
360 win32con = __import__('win32con')
361 except ImportError:
362 has_win32api = False
363 else:
364 # On POSIX systems, we need the x-bit set on the directory to access it,
365 # the r-bit to see its contents, and the w-bit to remove files from it.
366 # The actual modes of the files within the directory is irrelevant.
367 os.chmod(file_path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
368 for fn in os.listdir(file_path):
369 fullpath = os.path.join(file_path, fn)
370
371 # If fullpath is a symbolic link that points to a directory, isdir will
372 # be True, but we don't want to descend into that as a directory, we just
373 # want to remove the link. Check islink and treat links as ordinary files
374 # would be treated regardless of what they reference.
375 if os.path.islink(fullpath) or not os.path.isdir(fullpath):
376 if sys.platform == 'win32':
377 os.chmod(fullpath, stat.S_IWRITE)
378 if has_win32api:
379 win32api.SetFileAttributes(fullpath, win32con.FILE_ATTRIBUTE_NORMAL)
380 try:
381 os.remove(fullpath)
382 except OSError, e:
383 if e.errno != errno.EACCES or sys.platform != 'win32':
384 raise
385 print 'Failed to delete %s: trying again' % fullpath
386 time.sleep(0.1)
387 os.remove(fullpath)
388 else:
389 RemoveDirectory(fullpath)
390
391 if sys.platform == 'win32':
392 os.chmod(file_path, stat.S_IWRITE)
393 if has_win32api:
394 win32api.SetFileAttributes(file_path, win32con.FILE_ATTRIBUTE_NORMAL)
395 try:
396 os.rmdir(file_path)
397 except OSError, e:
398 if e.errno != errno.EACCES or sys.platform != 'win32':
399 raise
400 print 'Failed to remove %s: trying again' % file_path
401 time.sleep(0.1)
402 os.rmdir(file_path)
403
404
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000405def SubprocessCall(command, in_directory, fail_status=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000406 """Runs command, a list, in directory in_directory.
407
408 This function wraps SubprocessCallAndCapture, but does not perform the
409 capturing functions. See that function for a more complete usage
410 description.
411 """
412 # Call subprocess and capture nothing:
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000413 SubprocessCallAndCapture(command, in_directory, fail_status)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000414
415
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000416def SubprocessCallAndCapture(command, in_directory, fail_status=None,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000417 pattern=None, capture_list=None):
418 """Runs command, a list, in directory in_directory.
419
420 A message indicating what is being done, as well as the command's stdout,
421 is printed to out.
422
423 If a pattern is specified, any line in the output matching pattern will have
424 its first match group appended to capture_list.
425
426 If the command fails, as indicated by a nonzero exit status, gclient will
427 exit with an exit status of fail_status. If fail_status is None (the
428 default), gclient will raise an Error exception.
429 """
430
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000431 print("\n________ running \'%s\' in \'%s\'"
432 % (' '.join(command), in_directory))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000433
434 # *Sigh*: Windows needs shell=True, or else it won't search %PATH% for the
435 # executable, but shell=True makes subprocess on Linux fail when it's called
436 # with a list because it only tries to execute the first item in the list.
437 kid = subprocess.Popen(command, bufsize=0, cwd=in_directory,
438 shell=(sys.platform == 'win32'), stdout=subprocess.PIPE)
439
440 if pattern:
441 compiled_pattern = re.compile(pattern)
442
443 # Also, we need to forward stdout to prevent weird re-ordering of output.
444 # This has to be done on a per byte basis to make sure it is not buffered:
445 # normally buffering is done for each line, but if svn requests input, no
446 # end-of-line character is output after the prompt and it would not show up.
447 in_byte = kid.stdout.read(1)
448 in_line = ""
449 while in_byte:
450 if in_byte != "\r":
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000451 sys.stdout.write(in_byte)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000452 in_line += in_byte
453 if in_byte == "\n" and pattern:
454 match = compiled_pattern.search(in_line[:-1])
455 if match:
456 capture_list.append(match.group(1))
457 in_line = ""
458 in_byte = kid.stdout.read(1)
459 rv = kid.wait()
460
461 if rv:
462 msg = "failed to run command: %s" % " ".join(command)
463
464 if fail_status != None:
465 print >>sys.stderr, msg
466 sys.exit(fail_status)
467
468 raise Error(msg)
469
470
471def IsUsingGit(root, paths):
472 """Returns True if we're using git to manage any of our checkouts.
473 |entries| is a list of paths to check."""
474 for path in paths:
475 if os.path.exists(os.path.join(root, path, '.git')):
476 return True
477 return False
478
479# -----------------------------------------------------------------------------
480# SVN utils:
481
482
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000483def RunSVN(args, in_directory):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000484 """Runs svn, sending output to stdout.
485
486 Args:
487 args: A sequence of command line parameters to be passed to svn.
488 in_directory: The directory where svn is to be run.
489
490 Raises:
491 Error: An error occurred while running the svn command.
492 """
493 c = [SVN_COMMAND]
494 c.extend(args)
495
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000496 SubprocessCall(c, in_directory)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000497
498
maruel@chromium.org5c3a2ff2009-05-12 19:28:55 +0000499def CaptureSVN(args, in_directory=None, print_error=True):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000500 """Runs svn, capturing output sent to stdout as a string.
501
502 Args:
503 args: A sequence of command line parameters to be passed to svn.
504 in_directory: The directory where svn is to be run.
505
506 Returns:
507 The output sent to stdout as a string.
508 """
509 c = [SVN_COMMAND]
510 c.extend(args)
511
512 # *Sigh*: Windows needs shell=True, or else it won't search %PATH% for
513 # the svn.exe executable, but shell=True makes subprocess on Linux fail
514 # when it's called with a list because it only tries to execute the
515 # first string ("svn").
maruel@chromium.org5c3a2ff2009-05-12 19:28:55 +0000516 stderr = None
maruel@chromium.org672343d2009-05-20 20:03:25 +0000517 if not print_error:
maruel@chromium.org5c3a2ff2009-05-12 19:28:55 +0000518 stderr = subprocess.PIPE
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000519 return subprocess.Popen(c,
520 cwd=in_directory,
521 shell=(sys.platform == 'win32'),
maruel@chromium.org5c3a2ff2009-05-12 19:28:55 +0000522 stdout=subprocess.PIPE,
523 stderr=stderr).communicate()[0]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000524
525
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000526def RunSVNAndGetFileList(args, in_directory, file_list):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000527 """Runs svn checkout, update, or status, output to stdout.
528
529 The first item in args must be either "checkout", "update", or "status".
530
531 svn's stdout is parsed to collect a list of files checked out or updated.
532 These files are appended to file_list. svn's stdout is also printed to
533 sys.stdout as in RunSVN.
534
535 Args:
536 args: A sequence of command line parameters to be passed to svn.
537 in_directory: The directory where svn is to be run.
538
539 Raises:
540 Error: An error occurred while running the svn command.
541 """
542 command = [SVN_COMMAND]
543 command.extend(args)
544
545 # svn update and svn checkout use the same pattern: the first three columns
546 # are for file status, property status, and lock status. This is followed
547 # by two spaces, and then the path to the file.
548 update_pattern = '^... (.*)$'
549
550 # The first three columns of svn status are the same as for svn update and
551 # svn checkout. The next three columns indicate addition-with-history,
552 # switch, and remote lock status. This is followed by one space, and then
553 # the path to the file.
554 status_pattern = '^...... (.*)$'
555
556 # args[0] must be a supported command. This will blow up if it's something
557 # else, which is good. Note that the patterns are only effective when
558 # these commands are used in their ordinary forms, the patterns are invalid
559 # for "svn status --show-updates", for example.
560 pattern = {
561 'checkout': update_pattern,
562 'status': status_pattern,
563 'update': update_pattern,
564 }[args[0]]
565
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000566 SubprocessCallAndCapture(command,
567 in_directory,
568 pattern=pattern,
569 capture_list=file_list)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000570
571
maruel@chromium.org5c3a2ff2009-05-12 19:28:55 +0000572def CaptureSVNInfo(relpath, in_directory=None, print_error=True):
maruel@chromium.org2dc8a4d2009-05-11 12:41:20 +0000573 """Returns a dictionary from the svn info output for the given file.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000574
575 Args:
576 relpath: The directory where the working copy resides relative to
577 the directory given by in_directory.
578 in_directory: The directory where svn is to be run.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000579 """
maruel@chromium.org5c3a2ff2009-05-12 19:28:55 +0000580 output = CaptureSVN(["info", "--xml", relpath], in_directory, print_error)
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000581 dom = ParseXML(output)
maruel@chromium.org2dc8a4d2009-05-11 12:41:20 +0000582 result = {}
maruel@chromium.org483b0082009-05-07 02:57:14 +0000583 if dom:
maruel@chromium.org2dc8a4d2009-05-11 12:41:20 +0000584 def C(item, f):
585 if item is not None: return f(item)
maruel@chromium.org483b0082009-05-07 02:57:14 +0000586 # /info/entry/
587 # url
588 # reposityory/(root|uuid)
589 # wc-info/(schedule|depth)
590 # commit/(author|date)
591 # str() the results because they may be returned as Unicode, which
592 # interferes with the higher layers matching up things in the deps
593 # dictionary.
maruel@chromium.org2dc8a4d2009-05-11 12:41:20 +0000594 # TODO(maruel): Fix at higher level instead (!)
595 result['Repository Root'] = C(GetNamedNodeText(dom, 'root'), str)
596 result['URL'] = C(GetNamedNodeText(dom, 'url'), str)
597 result['UUID'] = C(GetNamedNodeText(dom, 'uuid'), str)
598 result['Revision'] = C(GetNodeNamedAttributeText(dom, 'entry', 'revision'),
599 int)
600 result['Node Kind'] = C(GetNodeNamedAttributeText(dom, 'entry', 'kind'),
601 str)
602 result['Schedule'] = C(GetNamedNodeText(dom, 'schedule'), str)
603 result['Path'] = C(GetNodeNamedAttributeText(dom, 'entry', 'path'), str)
604 result['Copied From URL'] = C(GetNamedNodeText(dom, 'copy-from-url'), str)
605 result['Copied From Rev'] = C(GetNamedNodeText(dom, 'copy-from-rev'), str)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000606 return result
607
608
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000609def CaptureSVNHeadRevision(url):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000610 """Get the head revision of a SVN repository.
611
612 Returns:
613 Int head revision
614 """
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000615 info = CaptureSVN(["info", "--xml", url], os.getcwd())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000616 dom = xml.dom.minidom.parseString(info)
617 return int(dom.getElementsByTagName('entry')[0].getAttribute('revision'))
618
619
maruel@chromium.org4810a962009-05-12 21:03:34 +0000620def CaptureSVNStatus(files):
621 """Returns the svn 1.5 svn status emulated output.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000622
maruel@chromium.org4810a962009-05-12 21:03:34 +0000623 @files can be a string (one file) or a list of files.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000624
maruel@chromium.org4810a962009-05-12 21:03:34 +0000625 Returns an array of (status, file) tuples."""
626 command = ["status", "--xml"]
627 if not files:
628 pass
629 elif isinstance(files, basestring):
630 command.append(files)
631 else:
632 command.extend(files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000633
maruel@chromium.org4810a962009-05-12 21:03:34 +0000634 status_letter = {
635 None: ' ',
636 '': ' ',
637 'added': 'A',
638 'conflicted': 'C',
639 'deleted': 'D',
640 'external': 'X',
641 'ignored': 'I',
642 'incomplete': '!',
643 'merged': 'G',
644 'missing': '!',
645 'modified': 'M',
646 'none': ' ',
647 'normal': ' ',
648 'obstructed': '~',
649 'replaced': 'R',
650 'unversioned': '?',
651 }
652 dom = ParseXML(CaptureSVN(command))
maruel@chromium.orge105d8d2009-04-30 17:58:25 +0000653 results = []
654 if dom:
655 # /status/target/entry/(wc-status|commit|author|date)
656 for target in dom.getElementsByTagName('target'):
657 base_path = target.getAttribute('path')
658 for entry in target.getElementsByTagName('entry'):
659 file = entry.getAttribute('path')
660 wc_status = entry.getElementsByTagName('wc-status')
661 assert len(wc_status) == 1
662 # Emulate svn 1.5 status ouput...
663 statuses = [' ' for i in range(7)]
664 # Col 0
665 xml_item_status = wc_status[0].getAttribute('item')
maruel@chromium.org4810a962009-05-12 21:03:34 +0000666 if xml_item_status in status_letter:
667 statuses[0] = status_letter[xml_item_status]
maruel@chromium.orge105d8d2009-04-30 17:58:25 +0000668 else:
669 raise Exception('Unknown item status "%s"; please implement me!' %
670 xml_item_status)
671 # Col 1
672 xml_props_status = wc_status[0].getAttribute('props')
673 if xml_props_status == 'modified':
674 statuses[1] = 'M'
675 elif xml_props_status == 'conflicted':
676 statuses[1] = 'C'
677 elif (not xml_props_status or xml_props_status == 'none' or
678 xml_props_status == 'normal'):
679 pass
680 else:
681 raise Exception('Unknown props status "%s"; please implement me!' %
682 xml_props_status)
maruel@chromium.orgedd27d12009-05-01 17:46:56 +0000683 # Col 2
684 if wc_status[0].getAttribute('wc-locked') == 'true':
685 statuses[2] = 'L'
maruel@chromium.orge105d8d2009-04-30 17:58:25 +0000686 # Col 3
687 if wc_status[0].getAttribute('copied') == 'true':
688 statuses[3] = '+'
maruel@chromium.org4810a962009-05-12 21:03:34 +0000689 item = (''.join(statuses), file)
maruel@chromium.orge105d8d2009-04-30 17:58:25 +0000690 results.append(item)
691 return results
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000692
693
694### SCM abstraction layer
695
696
697class SCMWrapper(object):
698 """Add necessary glue between all the supported SCM.
699
700 This is the abstraction layer to bind to different SCM. Since currently only
701 subversion is supported, a lot of subersionism remains. This can be sorted out
702 once another SCM is supported."""
703 def __init__(self, url=None, root_dir=None, relpath=None,
704 scm_name='svn'):
705 # TODO(maruel): Deduce the SCM from the url.
706 self.scm_name = scm_name
707 self.url = url
708 self._root_dir = root_dir
709 if self._root_dir:
maruel@chromium.orge105d8d2009-04-30 17:58:25 +0000710 self._root_dir = self._root_dir.replace('/', os.sep)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000711 self.relpath = relpath
712 if self.relpath:
maruel@chromium.orge105d8d2009-04-30 17:58:25 +0000713 self.relpath = self.relpath.replace('/', os.sep)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000714
715 def FullUrlForRelativeUrl(self, url):
716 # Find the forth '/' and strip from there. A bit hackish.
717 return '/'.join(self.url.split('/')[:4]) + url
718
719 def RunCommand(self, command, options, args, file_list=None):
720 # file_list will have all files that are modified appended to it.
721
722 if file_list == None:
723 file_list = []
724
725 commands = {
726 'cleanup': self.cleanup,
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +0000727 'export': self.export,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000728 'update': self.update,
729 'revert': self.revert,
730 'status': self.status,
731 'diff': self.diff,
732 'runhooks': self.status,
733 }
734
735 if not command in commands:
736 raise Error('Unknown command %s' % command)
737
738 return commands[command](options, args, file_list)
739
740 def cleanup(self, options, args, file_list):
741 """Cleanup working copy."""
742 command = ['cleanup']
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 diff(self, options, args, file_list):
747 # NOTE: This function does not currently modify file_list.
748 command = ['diff']
749 command.extend(args)
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000750 RunSVN(command, os.path.join(self._root_dir, self.relpath))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000751
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +0000752 def export(self, options, args, file_list):
753 assert len(args) == 1
754 export_path = os.path.abspath(os.path.join(args[0], self.relpath))
755 try:
756 os.makedirs(export_path)
757 except OSError:
758 pass
759 assert os.path.exists(export_path)
760 command = ['export', '--force', '.']
761 command.append(export_path)
762 RunSVN(command, os.path.join(self._root_dir, self.relpath))
763
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000764 def update(self, options, args, file_list):
765 """Runs SCM to update or transparently checkout the working copy.
766
767 All updated files will be appended to file_list.
768
769 Raises:
770 Error: if can't get URL for relative path.
771 """
772 # Only update if git is not controlling the directory.
maruel@chromium.org8626ff72009-05-13 02:57:02 +0000773 checkout_path = os.path.join(self._root_dir, self.relpath)
maruel@chromium.org0329e672009-05-13 18:41:04 +0000774 git_path = os.path.join(self._root_dir, self.relpath, '.git')
775 if os.path.exists(git_path):
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000776 print("________ found .git directory; skipping %s" % self.relpath)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000777 return
778
779 if args:
780 raise Error("Unsupported argument(s): %s" % ",".join(args))
781
782 url = self.url
783 components = url.split("@")
784 revision = None
785 forced_revision = False
786 if options.revision:
787 # Override the revision number.
788 url = '%s@%s' % (components[0], str(options.revision))
789 revision = int(options.revision)
790 forced_revision = True
791 elif len(components) == 2:
792 revision = int(components[1])
793 forced_revision = True
794
795 rev_str = ""
796 if revision:
797 rev_str = ' at %d' % revision
798
maruel@chromium.org0329e672009-05-13 18:41:04 +0000799 if not os.path.exists(checkout_path):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000800 # We need to checkout.
maruel@chromium.org8626ff72009-05-13 02:57:02 +0000801 command = ['checkout', url, checkout_path]
maruel@chromium.orgedd27d12009-05-01 17:46:56 +0000802 if revision:
803 command.extend(['--revision', str(revision)])
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000804 RunSVNAndGetFileList(command, self._root_dir, file_list)
maruel@chromium.orgedd27d12009-05-01 17:46:56 +0000805 return
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000806
807 # Get the existing scm url and the revision number of the current checkout.
maruel@chromium.org8626ff72009-05-13 02:57:02 +0000808 from_info = CaptureSVNInfo(os.path.join(checkout_path, '.'), '.')
maruel@chromium.org1998c6d2009-05-15 12:38:12 +0000809 if not from_info:
810 raise Error("Can't update/checkout %r if an unversioned directory is "
811 "present. Delete the directory and try again." %
812 checkout_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000813
814 if options.manually_grab_svn_rev:
815 # Retrieve the current HEAD version because svn is slow at null updates.
816 if not revision:
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000817 from_info_live = CaptureSVNInfo(from_info['URL'], '.')
maruel@chromium.org2dc8a4d2009-05-11 12:41:20 +0000818 revision = int(from_info_live['Revision'])
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000819 rev_str = ' at %d' % revision
820
maruel@chromium.org2dc8a4d2009-05-11 12:41:20 +0000821 if from_info['URL'] != components[0]:
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000822 to_info = CaptureSVNInfo(url, '.')
maruel@chromium.org8626ff72009-05-13 02:57:02 +0000823 can_switch = ((from_info['Repository Root'] != to_info['Repository Root'])
824 and (from_info['UUID'] == to_info['UUID']))
825 if can_switch:
826 print("\n_____ relocating %s to a new checkout" % self.relpath)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000827 # We have different roots, so check if we can switch --relocate.
828 # Subversion only permits this if the repository UUIDs match.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000829 # Perform the switch --relocate, then rewrite the from_url
830 # to reflect where we "are now." (This is the same way that
831 # Subversion itself handles the metadata when switch --relocate
832 # is used.) This makes the checks below for whether we
833 # can update to a revision or have to switch to a different
834 # branch work as expected.
835 # TODO(maruel): TEST ME !
maruel@chromium.org2dc8a4d2009-05-11 12:41:20 +0000836 command = ["switch", "--relocate",
837 from_info['Repository Root'],
838 to_info['Repository Root'],
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000839 self.relpath]
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000840 RunSVN(command, self._root_dir)
maruel@chromium.org2dc8a4d2009-05-11 12:41:20 +0000841 from_info['URL'] = from_info['URL'].replace(
842 from_info['Repository Root'],
843 to_info['Repository Root'])
maruel@chromium.org8626ff72009-05-13 02:57:02 +0000844 else:
845 if CaptureSVNStatus(checkout_path):
846 raise Error("Can't switch the checkout to %s; UUID don't match and "
847 "there is local changes in %s. Delete the directory and "
848 "try again." % (url, checkout_path))
849 # Ok delete it.
850 print("\n_____ switching %s to a new checkout" % self.relpath)
851 RemoveDirectory(checkout_path)
852 # We need to checkout.
853 command = ['checkout', url, checkout_path]
854 if revision:
855 command.extend(['--revision', str(revision)])
856 RunSVNAndGetFileList(command, self._root_dir, file_list)
857 return
858
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000859
860 # If the provided url has a revision number that matches the revision
861 # number of the existing directory, then we don't need to bother updating.
maruel@chromium.org2dc8a4d2009-05-11 12:41:20 +0000862 if not options.force and from_info['Revision'] == revision:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000863 if options.verbose or not forced_revision:
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000864 print("\n_____ %s%s" % (self.relpath, rev_str))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000865 return
866
maruel@chromium.org8626ff72009-05-13 02:57:02 +0000867 command = ["update", checkout_path]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000868 if revision:
869 command.extend(['--revision', str(revision)])
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000870 RunSVNAndGetFileList(command, self._root_dir, file_list)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000871
872 def revert(self, options, args, file_list):
873 """Reverts local modifications. Subversion specific.
874
875 All reverted files will be appended to file_list, even if Subversion
876 doesn't know about them.
877 """
878 path = os.path.join(self._root_dir, self.relpath)
879 if not os.path.isdir(path):
maruel@chromium.orgedd27d12009-05-01 17:46:56 +0000880 # svn revert won't work if the directory doesn't exist. It needs to
881 # checkout instead.
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000882 print("\n_____ %s is missing, synching instead" % self.relpath)
maruel@chromium.orgedd27d12009-05-01 17:46:56 +0000883 # Don't reuse the args.
884 return self.update(options, [], file_list)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000885
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000886 files = CaptureSVNStatus(path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000887 # Batch the command.
888 files_to_revert = []
889 for file in files:
maruel@chromium.org4810a962009-05-12 21:03:34 +0000890 file_path = os.path.join(path, file[1])
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000891 print(file_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000892 # Unversioned file or unexpected unversioned file.
maruel@chromium.org4810a962009-05-12 21:03:34 +0000893 if file[0][0] in ('?', '~'):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000894 # Remove extraneous file. Also remove unexpected unversioned
895 # directories. svn won't touch them but we want to delete these.
896 file_list.append(file_path)
897 try:
898 os.remove(file_path)
899 except EnvironmentError:
900 RemoveDirectory(file_path)
901
maruel@chromium.org4810a962009-05-12 21:03:34 +0000902 if file[0][0] != '?':
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000903 # For any other status, svn revert will work.
904 file_list.append(file_path)
maruel@chromium.org4810a962009-05-12 21:03:34 +0000905 files_to_revert.append(file[1])
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000906
907 # Revert them all at once.
908 if files_to_revert:
909 accumulated_paths = []
910 accumulated_length = 0
911 command = ['revert']
912 for p in files_to_revert:
913 # Some shell have issues with command lines too long.
914 if accumulated_length and accumulated_length + len(p) > 3072:
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000915 RunSVN(command + accumulated_paths,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000916 os.path.join(self._root_dir, self.relpath))
917 accumulated_paths = []
918 accumulated_length = 0
919 else:
920 accumulated_paths.append(p)
921 accumulated_length += len(p)
922 if accumulated_paths:
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000923 RunSVN(command + accumulated_paths,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000924 os.path.join(self._root_dir, self.relpath))
925
926 def status(self, options, args, file_list):
927 """Display status information."""
maruel@chromium.orgedd27d12009-05-01 17:46:56 +0000928 path = os.path.join(self._root_dir, self.relpath)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000929 command = ['status']
930 command.extend(args)
maruel@chromium.orgedd27d12009-05-01 17:46:56 +0000931 if not os.path.isdir(path):
932 # svn status won't work if the directory doesn't exist.
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000933 print("\n________ couldn't run \'%s\' in \'%s\':\nThe directory "
934 "does not exist."
935 % (' '.join(command), path))
maruel@chromium.orgedd27d12009-05-01 17:46:56 +0000936 # There's no file list to retrieve.
937 else:
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000938 RunSVNAndGetFileList(command, path, file_list)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000939
940
941## GClient implementation.
942
943
944class GClient(object):
945 """Object that represent a gclient checkout."""
946
947 supported_commands = [
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +0000948 'cleanup', 'diff', 'export', 'revert', 'status', 'update', 'runhooks'
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000949 ]
950
951 def __init__(self, root_dir, options):
952 self._root_dir = root_dir
953 self._options = options
954 self._config_content = None
955 self._config_dict = {}
956 self._deps_hooks = []
957
958 def SetConfig(self, content):
959 self._config_dict = {}
960 self._config_content = content
skylined@chromium.orgdf0032c2009-05-29 10:43:56 +0000961 try:
962 exec(content, self._config_dict)
963 except SyntaxError, e:
964 try:
965 # Try to construct a human readable error message
966 error_message = [
967 'There is a syntax error in your configuration file.',
968 'Line #%s, character %s:' % (e.lineno, e.offset),
969 '"%s"' % re.sub(r'[\r\n]*$', '', e.text) ]
970 except:
971 # Something went wrong, re-raise the original exception
972 raise e
973 else:
974 # Raise a new exception with the human readable message:
975 raise Error('\n'.join(error_message))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000976
977 def SaveConfig(self):
978 FileWrite(os.path.join(self._root_dir, self._options.config_filename),
979 self._config_content)
980
981 def _LoadConfig(self):
982 client_source = FileRead(os.path.join(self._root_dir,
983 self._options.config_filename))
984 self.SetConfig(client_source)
985
986 def ConfigContent(self):
987 return self._config_content
988
989 def GetVar(self, key, default=None):
990 return self._config_dict.get(key, default)
991
992 @staticmethod
993 def LoadCurrentConfig(options, from_dir=None):
994 """Searches for and loads a .gclient file relative to the current working
995 dir.
996
997 Returns:
998 A dict representing the contents of the .gclient file or an empty dict if
999 the .gclient file doesn't exist.
1000 """
1001 if not from_dir:
1002 from_dir = os.curdir
1003 path = os.path.realpath(from_dir)
maruel@chromium.org0329e672009-05-13 18:41:04 +00001004 while not os.path.exists(os.path.join(path, options.config_filename)):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001005 next = os.path.split(path)
1006 if not next[1]:
1007 return None
1008 path = next[0]
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001009 client = GClient(path, options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001010 client._LoadConfig()
1011 return client
1012
1013 def SetDefaultConfig(self, solution_name, solution_url, safesync_url):
1014 self.SetConfig(DEFAULT_CLIENT_FILE_TEXT % (
1015 solution_name, solution_url, safesync_url
1016 ))
1017
1018 def _SaveEntries(self, entries):
1019 """Creates a .gclient_entries file to record the list of unique checkouts.
1020
1021 The .gclient_entries file lives in the same directory as .gclient.
1022
1023 Args:
1024 entries: A sequence of solution names.
1025 """
1026 text = "entries = [\n"
1027 for entry in entries:
1028 text += " \"%s\",\n" % entry
1029 text += "]\n"
1030 FileWrite(os.path.join(self._root_dir, self._options.entries_filename),
1031 text)
1032
1033 def _ReadEntries(self):
1034 """Read the .gclient_entries file for the given client.
1035
1036 Args:
1037 client: The client for which the entries file should be read.
1038
1039 Returns:
1040 A sequence of solution names, which will be empty if there is the
1041 entries file hasn't been created yet.
1042 """
1043 scope = {}
1044 filename = os.path.join(self._root_dir, self._options.entries_filename)
maruel@chromium.org0329e672009-05-13 18:41:04 +00001045 if not os.path.exists(filename):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001046 return []
1047 exec(FileRead(filename), scope)
1048 return scope["entries"]
1049
1050 class FromImpl:
1051 """Used to implement the From syntax."""
1052
1053 def __init__(self, module_name):
1054 self.module_name = module_name
1055
1056 def __str__(self):
1057 return 'From("%s")' % self.module_name
1058
1059 class _VarImpl:
1060 def __init__(self, custom_vars, local_scope):
1061 self._custom_vars = custom_vars
1062 self._local_scope = local_scope
1063
1064 def Lookup(self, var_name):
1065 """Implements the Var syntax."""
1066 if var_name in self._custom_vars:
1067 return self._custom_vars[var_name]
1068 elif var_name in self._local_scope.get("vars", {}):
1069 return self._local_scope["vars"][var_name]
1070 raise Error("Var is not defined: %s" % var_name)
1071
1072 def _ParseSolutionDeps(self, solution_name, solution_deps_content,
1073 custom_vars):
1074 """Parses the DEPS file for the specified solution.
1075
1076 Args:
1077 solution_name: The name of the solution to query.
1078 solution_deps_content: Content of the DEPS file for the solution
1079 custom_vars: A dict of vars to override any vars defined in the DEPS file.
1080
1081 Returns:
1082 A dict mapping module names (as relative paths) to URLs or an empty
1083 dict if the solution does not have a DEPS file.
1084 """
1085 # Skip empty
1086 if not solution_deps_content:
1087 return {}
1088 # Eval the content
1089 local_scope = {}
1090 var = self._VarImpl(custom_vars, local_scope)
1091 global_scope = {"From": self.FromImpl, "Var": var.Lookup, "deps_os": {}}
1092 exec(solution_deps_content, global_scope, local_scope)
1093 deps = local_scope.get("deps", {})
1094
1095 # load os specific dependencies if defined. these dependencies may
1096 # override or extend the values defined by the 'deps' member.
1097 if "deps_os" in local_scope:
1098 deps_os_choices = {
1099 "win32": "win",
1100 "win": "win",
1101 "cygwin": "win",
1102 "darwin": "mac",
1103 "mac": "mac",
1104 "unix": "unix",
1105 "linux": "unix",
1106 "linux2": "unix",
1107 }
1108
1109 if self._options.deps_os is not None:
1110 deps_to_include = self._options.deps_os.split(",")
1111 if "all" in deps_to_include:
1112 deps_to_include = deps_os_choices.values()
1113 else:
1114 deps_to_include = [deps_os_choices.get(self._options.platform, "unix")]
1115
1116 deps_to_include = set(deps_to_include)
1117 for deps_os_key in deps_to_include:
1118 os_deps = local_scope["deps_os"].get(deps_os_key, {})
1119 if len(deps_to_include) > 1:
1120 # Ignore any overrides when including deps for more than one
1121 # platform, so we collect the broadest set of dependencies available.
1122 # We may end up with the wrong revision of something for our
1123 # platform, but this is the best we can do.
1124 deps.update([x for x in os_deps.items() if not x[0] in deps])
1125 else:
1126 deps.update(os_deps)
1127
1128 if 'hooks' in local_scope:
1129 self._deps_hooks.extend(local_scope['hooks'])
1130
1131 # If use_relative_paths is set in the DEPS file, regenerate
1132 # the dictionary using paths relative to the directory containing
1133 # the DEPS file.
1134 if local_scope.get('use_relative_paths'):
1135 rel_deps = {}
1136 for d, url in deps.items():
1137 # normpath is required to allow DEPS to use .. in their
1138 # dependency local path.
1139 rel_deps[os.path.normpath(os.path.join(solution_name, d))] = url
1140 return rel_deps
1141 else:
1142 return deps
1143
1144 def _ParseAllDeps(self, solution_urls, solution_deps_content):
1145 """Parse the complete list of dependencies for the client.
1146
1147 Args:
1148 solution_urls: A dict mapping module names (as relative paths) to URLs
1149 corresponding to the solutions specified by the client. This parameter
1150 is passed as an optimization.
1151 solution_deps_content: A dict mapping module names to the content
1152 of their DEPS files
1153
1154 Returns:
1155 A dict mapping module names (as relative paths) to URLs corresponding
1156 to the entire set of dependencies to checkout for the given client.
1157
1158 Raises:
1159 Error: If a dependency conflicts with another dependency or of a solution.
1160 """
1161 deps = {}
1162 for solution in self.GetVar("solutions"):
1163 custom_vars = solution.get("custom_vars", {})
1164 solution_deps = self._ParseSolutionDeps(
1165 solution["name"],
1166 solution_deps_content[solution["name"]],
1167 custom_vars)
1168
1169 # If a line is in custom_deps, but not in the solution, we want to append
1170 # this line to the solution.
1171 if "custom_deps" in solution:
1172 for d in solution["custom_deps"]:
1173 if d not in solution_deps:
1174 solution_deps[d] = solution["custom_deps"][d]
1175
1176 for d in solution_deps:
1177 if "custom_deps" in solution and d in solution["custom_deps"]:
1178 # Dependency is overriden.
1179 url = solution["custom_deps"][d]
1180 if url is None:
1181 continue
1182 else:
1183 url = solution_deps[d]
1184 # if we have a From reference dependent on another solution, then
1185 # just skip the From reference. When we pull deps for the solution,
1186 # we will take care of this dependency.
1187 #
1188 # If multiple solutions all have the same From reference, then we
1189 # should only add one to our list of dependencies.
1190 if type(url) != str:
1191 if url.module_name in solution_urls:
1192 # Already parsed.
1193 continue
1194 if d in deps and type(deps[d]) != str:
1195 if url.module_name == deps[d].module_name:
1196 continue
1197 else:
1198 parsed_url = urlparse.urlparse(url)
1199 scheme = parsed_url[0]
1200 if not scheme:
1201 # A relative url. Fetch the real base.
1202 path = parsed_url[2]
1203 if path[0] != "/":
1204 raise Error(
1205 "relative DEPS entry \"%s\" must begin with a slash" % d)
1206 # Create a scm just to query the full url.
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001207 scm = SCMWrapper(solution["url"], self._root_dir, None)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001208 url = scm.FullUrlForRelativeUrl(url)
1209 if d in deps and deps[d] != url:
1210 raise Error(
1211 "Solutions have conflicting versions of dependency \"%s\"" % d)
1212 if d in solution_urls and solution_urls[d] != url:
1213 raise Error(
1214 "Dependency \"%s\" conflicts with specified solution" % d)
1215 # Grab the dependency.
1216 deps[d] = url
1217 return deps
1218
1219 def _RunHookAction(self, hook_dict):
1220 """Runs the action from a single hook.
1221 """
1222 command = hook_dict['action'][:]
1223 if command[0] == 'python':
1224 # If the hook specified "python" as the first item, the action is a
1225 # Python script. Run it by starting a new copy of the same
1226 # interpreter.
1227 command[0] = sys.executable
1228
1229 # Use a discrete exit status code of 2 to indicate that a hook action
1230 # failed. Users of this script may wish to treat hook action failures
1231 # differently from VC failures.
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +00001232 SubprocessCall(command, self._root_dir, fail_status=2)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001233
1234 def _RunHooks(self, command, file_list, is_using_git):
1235 """Evaluates all hooks, running actions as needed.
1236 """
1237 # Hooks only run for these command types.
1238 if not command in ('update', 'revert', 'runhooks'):
1239 return
1240
1241 # Get any hooks from the .gclient file.
1242 hooks = self.GetVar("hooks", [])
1243 # Add any hooks found in DEPS files.
1244 hooks.extend(self._deps_hooks)
1245
1246 # If "--force" was specified, run all hooks regardless of what files have
1247 # changed. If the user is using git, then we don't know what files have
1248 # changed so we always run all hooks.
1249 if self._options.force or is_using_git:
1250 for hook_dict in hooks:
1251 self._RunHookAction(hook_dict)
1252 return
1253
1254 # Run hooks on the basis of whether the files from the gclient operation
1255 # match each hook's pattern.
1256 for hook_dict in hooks:
1257 pattern = re.compile(hook_dict['pattern'])
1258 for file in file_list:
1259 if not pattern.search(file):
1260 continue
1261
1262 self._RunHookAction(hook_dict)
1263
1264 # The hook's action only runs once. Don't bother looking for any
1265 # more matches.
1266 break
1267
1268 def RunOnDeps(self, command, args):
1269 """Runs a command on each dependency in a client and its dependencies.
1270
1271 The module's dependencies are specified in its top-level DEPS files.
1272
1273 Args:
1274 command: The command to use (e.g., 'status' or 'diff')
1275 args: list of str - extra arguments to add to the command line.
1276
1277 Raises:
1278 Error: If the client has conflicting entries.
1279 """
1280 if not command in self.supported_commands:
1281 raise Error("'%s' is an unsupported command" % command)
1282
1283 # Check for revision overrides.
1284 revision_overrides = {}
1285 for revision in self._options.revisions:
1286 if revision.find("@") == -1:
1287 raise Error(
1288 "Specify the full dependency when specifying a revision number.")
1289 revision_elem = revision.split("@")
1290 # Disallow conflicting revs
1291 if revision_overrides.has_key(revision_elem[0]) and \
1292 revision_overrides[revision_elem[0]] != revision_elem[1]:
1293 raise Error(
1294 "Conflicting revision numbers specified.")
1295 revision_overrides[revision_elem[0]] = revision_elem[1]
1296
1297 solutions = self.GetVar("solutions")
1298 if not solutions:
1299 raise Error("No solution specified")
1300
1301 # When running runhooks --force, there's no need to consult the SCM.
1302 # All known hooks are expected to run unconditionally regardless of working
1303 # copy state, so skip the SCM status check.
1304 run_scm = not (command == 'runhooks' and self._options.force)
1305
1306 entries = {}
1307 entries_deps_content = {}
1308 file_list = []
1309 # Run on the base solutions first.
1310 for solution in solutions:
1311 name = solution["name"]
1312 if name in entries:
1313 raise Error("solution %s specified more than once" % name)
1314 url = solution["url"]
1315 entries[name] = url
1316 if run_scm:
1317 self._options.revision = revision_overrides.get(name)
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001318 scm = SCMWrapper(url, self._root_dir, name)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001319 scm.RunCommand(command, self._options, args, file_list)
1320 self._options.revision = None
1321 try:
1322 deps_content = FileRead(os.path.join(self._root_dir, name,
1323 self._options.deps_file))
1324 except IOError, e:
1325 if e.errno != errno.ENOENT:
1326 raise
1327 deps_content = ""
1328 entries_deps_content[name] = deps_content
1329
1330 # Process the dependencies next (sort alphanumerically to ensure that
1331 # containing directories get populated first and for readability)
1332 deps = self._ParseAllDeps(entries, entries_deps_content)
1333 deps_to_process = deps.keys()
1334 deps_to_process.sort()
1335
1336 # First pass for direct dependencies.
1337 for d in deps_to_process:
1338 if type(deps[d]) == str:
1339 url = deps[d]
1340 entries[d] = url
1341 if run_scm:
1342 self._options.revision = revision_overrides.get(d)
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001343 scm = SCMWrapper(url, self._root_dir, d)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001344 scm.RunCommand(command, self._options, args, file_list)
1345 self._options.revision = None
1346
1347 # Second pass for inherited deps (via the From keyword)
1348 for d in deps_to_process:
1349 if type(deps[d]) != str:
1350 sub_deps = self._ParseSolutionDeps(
1351 deps[d].module_name,
1352 FileRead(os.path.join(self._root_dir,
1353 deps[d].module_name,
1354 self._options.deps_file)),
1355 {})
1356 url = sub_deps[d]
1357 entries[d] = url
1358 if run_scm:
1359 self._options.revision = revision_overrides.get(d)
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001360 scm = SCMWrapper(url, self._root_dir, d)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001361 scm.RunCommand(command, self._options, args, file_list)
1362 self._options.revision = None
1363
1364 is_using_git = IsUsingGit(self._root_dir, entries.keys())
1365 self._RunHooks(command, file_list, is_using_git)
1366
1367 if command == 'update':
ajwong@chromium.orgcdcee802009-06-23 15:30:42 +00001368 # Notify the user if there is an orphaned entry in their working copy.
1369 # Only delete the directory if there are no changes in it, and
1370 # delete_unversioned_trees is set to true.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001371 prev_entries = self._ReadEntries()
1372 for entry in prev_entries:
1373 e_dir = os.path.join(self._root_dir, entry)
maruel@chromium.org0329e672009-05-13 18:41:04 +00001374 if entry not in entries and os.path.exists(e_dir):
ajwong@chromium.org8399dc02009-06-23 21:36:25 +00001375 if not self._options.delete_unversioned_trees or \
1376 CaptureSVNStatus(e_dir):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001377 # There are modified files in this entry
1378 entries[entry] = None # Keep warning until removed.
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +00001379 print("\nWARNING: \"%s\" is no longer part of this client. "
1380 "It is recommended that you manually remove it.\n") % entry
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001381 else:
1382 # Delete the entry
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +00001383 print("\n________ deleting \'%s\' " +
1384 "in \'%s\'") % (entry, self._root_dir)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001385 RemoveDirectory(e_dir)
1386 # record the current list of entries for next time
1387 self._SaveEntries(entries)
1388
1389 def PrintRevInfo(self):
1390 """Output revision info mapping for the client and its dependencies. This
1391 allows the capture of a overall "revision" for the source tree that can
1392 be used to reproduce the same tree in the future. The actual output
1393 contains enough information (source paths, svn server urls and revisions)
1394 that it can be used either to generate external svn commands (without
1395 gclient) or as input to gclient's --rev option (with some massaging of
1396 the data).
1397
1398 NOTE: Unlike RunOnDeps this does not require a local checkout and is run
1399 on the Pulse master. It MUST NOT execute hooks.
1400
1401 Raises:
1402 Error: If the client has conflicting entries.
1403 """
1404 # Check for revision overrides.
1405 revision_overrides = {}
1406 for revision in self._options.revisions:
1407 if revision.find("@") < 0:
1408 raise Error(
1409 "Specify the full dependency when specifying a revision number.")
1410 revision_elem = revision.split("@")
1411 # Disallow conflicting revs
1412 if revision_overrides.has_key(revision_elem[0]) and \
1413 revision_overrides[revision_elem[0]] != revision_elem[1]:
1414 raise Error(
1415 "Conflicting revision numbers specified.")
1416 revision_overrides[revision_elem[0]] = revision_elem[1]
1417
1418 solutions = self.GetVar("solutions")
1419 if not solutions:
1420 raise Error("No solution specified")
1421
1422 entries = {}
1423 entries_deps_content = {}
1424
1425 # Inner helper to generate base url and rev tuple (including honoring
1426 # |revision_overrides|)
1427 def GetURLAndRev(name, original_url):
1428 if original_url.find("@") < 0:
1429 if revision_overrides.has_key(name):
1430 return (original_url, int(revision_overrides[name]))
1431 else:
1432 # TODO(aharper): SVN/SCMWrapper cleanup (non-local commandset)
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +00001433 return (original_url, CaptureSVNHeadRevision(original_url))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001434 else:
1435 url_components = original_url.split("@")
1436 if revision_overrides.has_key(name):
1437 return (url_components[0], int(revision_overrides[name]))
1438 else:
1439 return (url_components[0], int(url_components[1]))
1440
1441 # Run on the base solutions first.
1442 for solution in solutions:
1443 name = solution["name"]
1444 if name in entries:
1445 raise Error("solution %s specified more than once" % name)
1446 (url, rev) = GetURLAndRev(name, solution["url"])
1447 entries[name] = "%s@%d" % (url, rev)
1448 # TODO(aharper): SVN/SCMWrapper cleanup (non-local commandset)
1449 entries_deps_content[name] = CaptureSVN(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001450 ["cat",
1451 "%s/%s@%d" % (url,
1452 self._options.deps_file,
1453 rev)],
1454 os.getcwd())
1455
1456 # Process the dependencies next (sort alphanumerically to ensure that
1457 # containing directories get populated first and for readability)
1458 deps = self._ParseAllDeps(entries, entries_deps_content)
1459 deps_to_process = deps.keys()
1460 deps_to_process.sort()
1461
1462 # First pass for direct dependencies.
1463 for d in deps_to_process:
1464 if type(deps[d]) == str:
1465 (url, rev) = GetURLAndRev(d, deps[d])
1466 entries[d] = "%s@%d" % (url, rev)
1467
1468 # Second pass for inherited deps (via the From keyword)
1469 for d in deps_to_process:
1470 if type(deps[d]) != str:
1471 deps_parent_url = entries[deps[d].module_name]
1472 if deps_parent_url.find("@") < 0:
1473 raise Error("From %s missing revisioned url" % deps[d].module_name)
1474 deps_parent_url_components = deps_parent_url.split("@")
1475 # TODO(aharper): SVN/SCMWrapper cleanup (non-local commandset)
1476 deps_parent_content = CaptureSVN(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001477 ["cat",
1478 "%s/%s@%s" % (deps_parent_url_components[0],
1479 self._options.deps_file,
1480 deps_parent_url_components[1])],
1481 os.getcwd())
1482 sub_deps = self._ParseSolutionDeps(
1483 deps[d].module_name,
1484 FileRead(os.path.join(self._root_dir,
1485 deps[d].module_name,
1486 self._options.deps_file)),
1487 {})
1488 (url, rev) = GetURLAndRev(d, sub_deps[d])
1489 entries[d] = "%s@%d" % (url, rev)
1490
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +00001491 print(";".join(["%s,%s" % (x, entries[x]) for x in sorted(entries.keys())]))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001492
1493
1494## gclient commands.
1495
1496
1497def DoCleanup(options, args):
1498 """Handle the cleanup subcommand.
1499
1500 Raises:
1501 Error: if client isn't configured properly.
1502 """
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001503 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001504 if not client:
1505 raise Error("client not configured; see 'gclient config'")
1506 if options.verbose:
1507 # Print out the .gclient file. This is longer than if we just printed the
1508 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +00001509 print(client.ConfigContent())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001510 options.verbose = True
1511 return client.RunOnDeps('cleanup', args)
1512
1513
1514def DoConfig(options, args):
1515 """Handle the config subcommand.
1516
1517 Args:
1518 options: If options.spec set, a string providing contents of config file.
1519 args: The command line args. If spec is not set,
1520 then args[0] is a string URL to get for config file.
1521
1522 Raises:
1523 Error: on usage error
1524 """
1525 if len(args) < 1 and not options.spec:
1526 raise Error("required argument missing; see 'gclient help config'")
maruel@chromium.org0329e672009-05-13 18:41:04 +00001527 if os.path.exists(options.config_filename):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001528 raise Error("%s file already exists in the current directory" %
1529 options.config_filename)
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001530 client = GClient('.', options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001531 if options.spec:
1532 client.SetConfig(options.spec)
1533 else:
1534 # TODO(darin): it would be nice to be able to specify an alternate relpath
1535 # for the given URL.
maruel@chromium.org1ab7ffc2009-06-03 17:21:37 +00001536 base_url = args[0].rstrip('/')
1537 name = base_url.split("/")[-1]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001538 safesync_url = ""
1539 if len(args) > 1:
1540 safesync_url = args[1]
1541 client.SetDefaultConfig(name, base_url, safesync_url)
1542 client.SaveConfig()
1543
1544
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +00001545def DoExport(options, args):
1546 """Handle the export subcommand.
1547
1548 Raises:
1549 Error: on usage error
1550 """
1551 if len(args) != 1:
1552 raise Error("Need directory name")
1553 client = GClient.LoadCurrentConfig(options)
1554
1555 if not client:
1556 raise Error("client not configured; see 'gclient config'")
1557
1558 if options.verbose:
1559 # Print out the .gclient file. This is longer than if we just printed the
1560 # client dict, but more legible, and it might contain helpful comments.
1561 print(client.ConfigContent())
1562 return client.RunOnDeps('export', args)
1563
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001564def DoHelp(options, args):
1565 """Handle the help subcommand giving help for another subcommand.
1566
1567 Raises:
1568 Error: if the command is unknown.
1569 """
1570 if len(args) == 1 and args[0] in COMMAND_USAGE_TEXT:
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +00001571 print(COMMAND_USAGE_TEXT[args[0]])
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001572 else:
1573 raise Error("unknown subcommand '%s'; see 'gclient help'" % args[0])
1574
1575
1576def DoStatus(options, args):
1577 """Handle the status subcommand.
1578
1579 Raises:
1580 Error: if client isn't configured properly.
1581 """
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001582 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001583 if not client:
1584 raise Error("client not configured; see 'gclient config'")
1585 if options.verbose:
1586 # Print out the .gclient file. This is longer than if we just printed the
1587 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +00001588 print(client.ConfigContent())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001589 options.verbose = True
1590 return client.RunOnDeps('status', args)
1591
1592
1593def DoUpdate(options, args):
1594 """Handle the update and sync subcommands.
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
1601 if not client:
1602 raise Error("client not configured; see 'gclient config'")
1603
1604 if not options.head:
1605 solutions = client.GetVar('solutions')
1606 if solutions:
1607 for s in solutions:
1608 if s.get('safesync_url', ''):
1609 # rip through revisions and make sure we're not over-riding
1610 # something that was explicitly passed
1611 has_key = False
1612 for r in options.revisions:
1613 if r.split('@')[0] == s['name']:
1614 has_key = True
1615 break
1616
1617 if not has_key:
1618 handle = urllib.urlopen(s['safesync_url'])
1619 rev = handle.read().strip()
1620 handle.close()
1621 if len(rev):
1622 options.revisions.append(s['name']+'@'+rev)
1623
1624 if options.verbose:
1625 # Print out the .gclient file. This is longer than if we just printed the
1626 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +00001627 print(client.ConfigContent())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001628 return client.RunOnDeps('update', args)
1629
1630
1631def DoDiff(options, args):
1632 """Handle the diff subcommand.
1633
1634 Raises:
1635 Error: if client isn't configured properly.
1636 """
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001637 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001638 if not client:
1639 raise Error("client not configured; see 'gclient config'")
1640 if options.verbose:
1641 # Print out the .gclient file. This is longer than if we just printed the
1642 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +00001643 print(client.ConfigContent())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001644 options.verbose = True
1645 return client.RunOnDeps('diff', args)
1646
1647
1648def DoRevert(options, args):
1649 """Handle the revert subcommand.
1650
1651 Raises:
1652 Error: if client isn't configured properly.
1653 """
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001654 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001655 if not client:
1656 raise Error("client not configured; see 'gclient config'")
1657 return client.RunOnDeps('revert', args)
1658
1659
1660def DoRunHooks(options, args):
1661 """Handle the runhooks subcommand.
1662
1663 Raises:
1664 Error: if client isn't configured properly.
1665 """
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001666 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001667 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.
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +00001672 print(client.ConfigContent())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001673 return client.RunOnDeps('runhooks', args)
1674
1675
1676def DoRevInfo(options, args):
1677 """Handle the revinfo subcommand.
1678
1679 Raises:
1680 Error: if client isn't configured properly.
1681 """
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001682 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001683 if not client:
1684 raise Error("client not configured; see 'gclient config'")
1685 client.PrintRevInfo()
1686
1687
1688gclient_command_map = {
1689 "cleanup": DoCleanup,
1690 "config": DoConfig,
1691 "diff": DoDiff,
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +00001692 "export": DoExport,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001693 "help": DoHelp,
1694 "status": DoStatus,
1695 "sync": DoUpdate,
1696 "update": DoUpdate,
1697 "revert": DoRevert,
1698 "runhooks": DoRunHooks,
1699 "revinfo" : DoRevInfo,
1700}
1701
1702
1703def DispatchCommand(command, options, args, command_map=None):
1704 """Dispatches the appropriate subcommand based on command line arguments."""
1705 if command_map is None:
1706 command_map = gclient_command_map
1707
1708 if command in command_map:
1709 return command_map[command](options, args)
1710 else:
1711 raise Error("unknown subcommand '%s'; see 'gclient help'" % command)
1712
1713
1714def Main(argv):
1715 """Parse command line arguments and dispatch command."""
1716
1717 option_parser = optparse.OptionParser(usage=DEFAULT_USAGE_TEXT,
1718 version=__version__)
1719 option_parser.disable_interspersed_args()
1720 option_parser.add_option("", "--force", action="store_true", default=False,
1721 help=("(update/sync only) force update even "
1722 "for modules which haven't changed"))
1723 option_parser.add_option("", "--revision", action="append", dest="revisions",
1724 metavar="REV", default=[],
1725 help=("(update/sync only) sync to a specific "
1726 "revision, can be used multiple times for "
1727 "each solution, e.g. --revision=src@123, "
1728 "--revision=internal@32"))
1729 option_parser.add_option("", "--deps", default=None, dest="deps_os",
1730 metavar="OS_LIST",
1731 help=("(update/sync only) sync deps for the "
1732 "specified (comma-separated) platform(s); "
1733 "'all' will sync all platforms"))
1734 option_parser.add_option("", "--spec", default=None,
1735 help=("(config only) create a gclient file "
1736 "containing the provided string"))
1737 option_parser.add_option("", "--verbose", action="store_true", default=False,
1738 help="produce additional output for diagnostics")
1739 option_parser.add_option("", "--manually_grab_svn_rev", action="store_true",
1740 default=False,
1741 help="Skip svn up whenever possible by requesting "
1742 "actual HEAD revision from the repository")
1743 option_parser.add_option("", "--head", action="store_true", default=False,
1744 help=("skips any safesync_urls specified in "
1745 "configured solutions"))
ajwong@chromium.orgcdcee802009-06-23 15:30:42 +00001746 option_parser.add_option("", "--delete_unversioned_trees",
1747 action="store_true", default=False,
1748 help=("on update, delete any unexpected "
1749 "unversioned trees that are in the checkout"))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001750
1751 if len(argv) < 2:
1752 # Users don't need to be told to use the 'help' command.
1753 option_parser.print_help()
1754 return 1
1755 # Add manual support for --version as first argument.
1756 if argv[1] == '--version':
1757 option_parser.print_version()
1758 return 0
1759
1760 # Add manual support for --help as first argument.
1761 if argv[1] == '--help':
1762 argv[1] = 'help'
1763
1764 command = argv[1]
1765 options, args = option_parser.parse_args(argv[2:])
1766
1767 if len(argv) < 3 and command == "help":
1768 option_parser.print_help()
1769 return 0
1770
1771 # Files used for configuration and state saving.
1772 options.config_filename = os.environ.get("GCLIENT_FILE", ".gclient")
1773 options.entries_filename = ".gclient_entries"
1774 options.deps_file = "DEPS"
1775
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001776 options.platform = sys.platform
1777 return DispatchCommand(command, options, args)
1778
1779
1780if "__main__" == __name__:
1781 try:
1782 result = Main(sys.argv)
1783 except Error, e:
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +00001784 print >> sys.stderr, "Error: %s" % str(e)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001785 result = 1
1786 sys.exit(result)
1787
1788# vim: ts=2:sw=2:tw=80:et: