blob: 54e94889b0f18537356e2e8ca7bae143cc36fed3 [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)"
67__version__ = "0.3.1"
68
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
261
maruel@chromium.orge105d8d2009-04-30 17:58:25 +0000262def getText(nodelist):
263 """
264 Return the concatenated text for the children of a list of DOM nodes.
265 """
266 rc = []
267 for node in nodelist:
268 if node.nodeType == node.TEXT_NODE:
269 rc.append(node.data)
270 else:
271 rc.append(getText(node.childNodes))
272 return ''.join(rc)
273
274
275def ParseXML(output):
276 try:
277 return xml.dom.minidom.parseString(output)
278 except xml.parsers.expat.ExpatError:
279 return None
280
281
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000282class Error(Exception):
283 """gclient exception class."""
284 pass
285
286class PrintableObject(object):
287 def __str__(self):
288 output = ''
289 for i in dir(self):
290 if i.startswith('__'):
291 continue
292 output += '%s = %s\n' % (i, str(getattr(self, i, '')))
293 return output
294
295
296def FileRead(filename):
297 content = None
298 f = open(filename, "rU")
299 try:
300 content = f.read()
301 finally:
302 f.close()
303 return content
304
305
306def FileWrite(filename, content):
307 f = open(filename, "w")
308 try:
309 f.write(content)
310 finally:
311 f.close()
312
313
314def RemoveDirectory(*path):
315 """Recursively removes a directory, even if it's marked read-only.
316
317 Remove the directory located at *path, if it exists.
318
319 shutil.rmtree() doesn't work on Windows if any of the files or directories
320 are read-only, which svn repositories and some .svn files are. We need to
321 be able to force the files to be writable (i.e., deletable) as we traverse
322 the tree.
323
324 Even with all this, Windows still sometimes fails to delete a file, citing
325 a permission error (maybe something to do with antivirus scans or disk
326 indexing). The best suggestion any of the user forums had was to wait a
327 bit and try again, so we do that too. It's hand-waving, but sometimes it
328 works. :/
329
330 On POSIX systems, things are a little bit simpler. The modes of the files
331 to be deleted doesn't matter, only the modes of the directories containing
332 them are significant. As the directory tree is traversed, each directory
333 has its mode set appropriately before descending into it. This should
334 result in the entire tree being removed, with the possible exception of
335 *path itself, because nothing attempts to change the mode of its parent.
336 Doing so would be hazardous, as it's not a directory slated for removal.
337 In the ordinary case, this is not a problem: for our purposes, the user
338 will never lack write permission on *path's parent.
339 """
340 file_path = os.path.join(*path)
341 if not os.path.exists(file_path):
342 return
343
344 if os.path.islink(file_path) or not os.path.isdir(file_path):
345 raise Error("RemoveDirectory asked to remove non-directory %s" % file_path)
346
347 has_win32api = False
348 if sys.platform == 'win32':
349 has_win32api = True
350 # Some people don't have the APIs installed. In that case we'll do without.
351 try:
352 win32api = __import__('win32api')
353 win32con = __import__('win32con')
354 except ImportError:
355 has_win32api = False
356 else:
357 # On POSIX systems, we need the x-bit set on the directory to access it,
358 # the r-bit to see its contents, and the w-bit to remove files from it.
359 # The actual modes of the files within the directory is irrelevant.
360 os.chmod(file_path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
361 for fn in os.listdir(file_path):
362 fullpath = os.path.join(file_path, fn)
363
364 # If fullpath is a symbolic link that points to a directory, isdir will
365 # be True, but we don't want to descend into that as a directory, we just
366 # want to remove the link. Check islink and treat links as ordinary files
367 # would be treated regardless of what they reference.
368 if os.path.islink(fullpath) or not os.path.isdir(fullpath):
369 if sys.platform == 'win32':
370 os.chmod(fullpath, stat.S_IWRITE)
371 if has_win32api:
372 win32api.SetFileAttributes(fullpath, win32con.FILE_ATTRIBUTE_NORMAL)
373 try:
374 os.remove(fullpath)
375 except OSError, e:
376 if e.errno != errno.EACCES or sys.platform != 'win32':
377 raise
378 print 'Failed to delete %s: trying again' % fullpath
379 time.sleep(0.1)
380 os.remove(fullpath)
381 else:
382 RemoveDirectory(fullpath)
383
384 if sys.platform == 'win32':
385 os.chmod(file_path, stat.S_IWRITE)
386 if has_win32api:
387 win32api.SetFileAttributes(file_path, win32con.FILE_ATTRIBUTE_NORMAL)
388 try:
389 os.rmdir(file_path)
390 except OSError, e:
391 if e.errno != errno.EACCES or sys.platform != 'win32':
392 raise
393 print 'Failed to remove %s: trying again' % file_path
394 time.sleep(0.1)
395 os.rmdir(file_path)
396
397
398def SubprocessCall(command, in_directory, out, fail_status=None):
399 """Runs command, a list, in directory in_directory.
400
401 This function wraps SubprocessCallAndCapture, but does not perform the
402 capturing functions. See that function for a more complete usage
403 description.
404 """
405 # Call subprocess and capture nothing:
406 SubprocessCallAndCapture(command, in_directory, out, fail_status)
407
408
409def SubprocessCallAndCapture(command, in_directory, out, fail_status=None,
410 pattern=None, capture_list=None):
411 """Runs command, a list, in directory in_directory.
412
413 A message indicating what is being done, as well as the command's stdout,
414 is printed to out.
415
416 If a pattern is specified, any line in the output matching pattern will have
417 its first match group appended to capture_list.
418
419 If the command fails, as indicated by a nonzero exit status, gclient will
420 exit with an exit status of fail_status. If fail_status is None (the
421 default), gclient will raise an Error exception.
422 """
423
424 print >> out, ("\n________ running \'%s\' in \'%s\'"
425 % (' '.join(command), in_directory))
426
427 # *Sigh*: Windows needs shell=True, or else it won't search %PATH% for the
428 # executable, but shell=True makes subprocess on Linux fail when it's called
429 # with a list because it only tries to execute the first item in the list.
430 kid = subprocess.Popen(command, bufsize=0, cwd=in_directory,
431 shell=(sys.platform == 'win32'), stdout=subprocess.PIPE)
432
433 if pattern:
434 compiled_pattern = re.compile(pattern)
435
436 # Also, we need to forward stdout to prevent weird re-ordering of output.
437 # This has to be done on a per byte basis to make sure it is not buffered:
438 # normally buffering is done for each line, but if svn requests input, no
439 # end-of-line character is output after the prompt and it would not show up.
440 in_byte = kid.stdout.read(1)
441 in_line = ""
442 while in_byte:
443 if in_byte != "\r":
444 out.write(in_byte)
445 in_line += in_byte
446 if in_byte == "\n" and pattern:
447 match = compiled_pattern.search(in_line[:-1])
448 if match:
449 capture_list.append(match.group(1))
450 in_line = ""
451 in_byte = kid.stdout.read(1)
452 rv = kid.wait()
453
454 if rv:
455 msg = "failed to run command: %s" % " ".join(command)
456
457 if fail_status != None:
458 print >>sys.stderr, msg
459 sys.exit(fail_status)
460
461 raise Error(msg)
462
463
464def IsUsingGit(root, paths):
465 """Returns True if we're using git to manage any of our checkouts.
466 |entries| is a list of paths to check."""
467 for path in paths:
468 if os.path.exists(os.path.join(root, path, '.git')):
469 return True
470 return False
471
472# -----------------------------------------------------------------------------
473# SVN utils:
474
475
476def RunSVN(options, args, in_directory):
477 """Runs svn, sending output to stdout.
478
479 Args:
480 args: A sequence of command line parameters to be passed to svn.
481 in_directory: The directory where svn is to be run.
482
483 Raises:
484 Error: An error occurred while running the svn command.
485 """
486 c = [SVN_COMMAND]
487 c.extend(args)
488
489 SubprocessCall(c, in_directory, options.stdout)
490
491
492def CaptureSVN(options, args, in_directory):
493 """Runs svn, capturing output sent to stdout as a string.
494
495 Args:
496 args: A sequence of command line parameters to be passed to svn.
497 in_directory: The directory where svn is to be run.
498
499 Returns:
500 The output sent to stdout as a string.
501 """
502 c = [SVN_COMMAND]
503 c.extend(args)
504
505 # *Sigh*: Windows needs shell=True, or else it won't search %PATH% for
506 # the svn.exe executable, but shell=True makes subprocess on Linux fail
507 # when it's called with a list because it only tries to execute the
508 # first string ("svn").
509 return subprocess.Popen(c, cwd=in_directory, shell=(sys.platform == 'win32'),
510 stdout=subprocess.PIPE).communicate()[0]
511
512
513def RunSVNAndGetFileList(options, args, in_directory, file_list):
514 """Runs svn checkout, update, or status, output to stdout.
515
516 The first item in args must be either "checkout", "update", or "status".
517
518 svn's stdout is parsed to collect a list of files checked out or updated.
519 These files are appended to file_list. svn's stdout is also printed to
520 sys.stdout as in RunSVN.
521
522 Args:
523 args: A sequence of command line parameters to be passed to svn.
524 in_directory: The directory where svn is to be run.
525
526 Raises:
527 Error: An error occurred while running the svn command.
528 """
529 command = [SVN_COMMAND]
530 command.extend(args)
531
532 # svn update and svn checkout use the same pattern: the first three columns
533 # are for file status, property status, and lock status. This is followed
534 # by two spaces, and then the path to the file.
535 update_pattern = '^... (.*)$'
536
537 # The first three columns of svn status are the same as for svn update and
538 # svn checkout. The next three columns indicate addition-with-history,
539 # switch, and remote lock status. This is followed by one space, and then
540 # the path to the file.
541 status_pattern = '^...... (.*)$'
542
543 # args[0] must be a supported command. This will blow up if it's something
544 # else, which is good. Note that the patterns are only effective when
545 # these commands are used in their ordinary forms, the patterns are invalid
546 # for "svn status --show-updates", for example.
547 pattern = {
548 'checkout': update_pattern,
549 'status': status_pattern,
550 'update': update_pattern,
551 }[args[0]]
552
553 SubprocessCallAndCapture(command, in_directory, options.stdout,
554 pattern=pattern, capture_list=file_list)
555
556
557def CaptureSVNInfo(options, relpath, in_directory):
558 """Runs 'svn info' on an existing path.
559
560 Args:
561 relpath: The directory where the working copy resides relative to
562 the directory given by in_directory.
563 in_directory: The directory where svn is to be run.
564
565 Returns:
566 An object with fields corresponding to the output of 'svn info'
567 """
568 info = CaptureSVN(options, ["info", "--xml", relpath], in_directory)
569 dom = xml.dom.minidom.parseString(info)
570
571 # str() the getText() results because they may be returned as
572 # Unicode, which interferes with the higher layers matching up
573 # things in the deps dictionary.
574 result = PrintableObject()
575 result.root = str(getText(dom.getElementsByTagName('root')))
576 result.url = str(getText(dom.getElementsByTagName('url')))
577 result.uuid = str(getText(dom.getElementsByTagName('uuid')))
578 result.revision = int(dom.getElementsByTagName('entry')[0].getAttribute(
579 'revision'))
580 return result
581
582
583def CaptureSVNHeadRevision(options, url):
584 """Get the head revision of a SVN repository.
585
586 Returns:
587 Int head revision
588 """
589 info = CaptureSVN(options, ["info", "--xml", url], os.getcwd())
590 dom = xml.dom.minidom.parseString(info)
591 return int(dom.getElementsByTagName('entry')[0].getAttribute('revision'))
592
593
594class FileStatus:
maruel@chromium.orge105d8d2009-04-30 17:58:25 +0000595 def __init__(self, path, text_status, props, history):
596 self.path = path
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000597 self.text_status = text_status
598 self.props = props
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000599 self.history = history
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000600
601 def __str__(self):
maruel@chromium.orge105d8d2009-04-30 17:58:25 +0000602 # Emulate svn status 1.5 output.
603 return (self.text_status + self.props + ' ' + self.history + ' ' +
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000604 self.path)
605
606
607def CaptureSVNStatus(options, path):
608 """Runs 'svn status' on an existing path.
609
610 Args:
611 path: The directory to run svn status.
612
613 Returns:
maruel@chromium.orge105d8d2009-04-30 17:58:25 +0000614 An array of FileStatus corresponding to the emulated output of 'svn status'
615 version 1.5."""
616 dom = ParseXML(CaptureSVN(options, ["status", "--xml"], path))
617 results = []
618 if dom:
619 # /status/target/entry/(wc-status|commit|author|date)
620 for target in dom.getElementsByTagName('target'):
621 base_path = target.getAttribute('path')
622 for entry in target.getElementsByTagName('entry'):
623 file = entry.getAttribute('path')
624 wc_status = entry.getElementsByTagName('wc-status')
625 assert len(wc_status) == 1
626 # Emulate svn 1.5 status ouput...
627 statuses = [' ' for i in range(7)]
628 # Col 0
629 xml_item_status = wc_status[0].getAttribute('item')
630 if xml_item_status == 'unversioned':
631 statuses[0] = '?'
632 elif xml_item_status == 'modified':
633 statuses[0] = 'M'
634 elif xml_item_status == 'added':
635 statuses[0] = 'A'
636 elif xml_item_status == 'conflicted':
637 statuses[0] = 'C'
638 elif not xml_item_status:
639 pass
640 else:
641 raise Exception('Unknown item status "%s"; please implement me!' %
642 xml_item_status)
643 # Col 1
644 xml_props_status = wc_status[0].getAttribute('props')
645 if xml_props_status == 'modified':
646 statuses[1] = 'M'
647 elif xml_props_status == 'conflicted':
648 statuses[1] = 'C'
649 elif (not xml_props_status or xml_props_status == 'none' or
650 xml_props_status == 'normal'):
651 pass
652 else:
653 raise Exception('Unknown props status "%s"; please implement me!' %
654 xml_props_status)
655 # Col 3
656 if wc_status[0].getAttribute('copied') == 'true':
657 statuses[3] = '+'
658 item = FileStatus(file, statuses[0], statuses[1], statuses[3])
659 results.append(item)
660 return results
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000661
662
663### SCM abstraction layer
664
665
666class SCMWrapper(object):
667 """Add necessary glue between all the supported SCM.
668
669 This is the abstraction layer to bind to different SCM. Since currently only
670 subversion is supported, a lot of subersionism remains. This can be sorted out
671 once another SCM is supported."""
672 def __init__(self, url=None, root_dir=None, relpath=None,
673 scm_name='svn'):
674 # TODO(maruel): Deduce the SCM from the url.
675 self.scm_name = scm_name
676 self.url = url
677 self._root_dir = root_dir
678 if self._root_dir:
maruel@chromium.orge105d8d2009-04-30 17:58:25 +0000679 self._root_dir = self._root_dir.replace('/', os.sep)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000680 self.relpath = relpath
681 if self.relpath:
maruel@chromium.orge105d8d2009-04-30 17:58:25 +0000682 self.relpath = self.relpath.replace('/', os.sep)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000683
684 def FullUrlForRelativeUrl(self, url):
685 # Find the forth '/' and strip from there. A bit hackish.
686 return '/'.join(self.url.split('/')[:4]) + url
687
688 def RunCommand(self, command, options, args, file_list=None):
689 # file_list will have all files that are modified appended to it.
690
691 if file_list == None:
692 file_list = []
693
694 commands = {
695 'cleanup': self.cleanup,
696 'update': self.update,
697 'revert': self.revert,
698 'status': self.status,
699 'diff': self.diff,
700 'runhooks': self.status,
701 }
702
703 if not command in commands:
704 raise Error('Unknown command %s' % command)
705
706 return commands[command](options, args, file_list)
707
708 def cleanup(self, options, args, file_list):
709 """Cleanup working copy."""
710 command = ['cleanup']
711 command.extend(args)
712 RunSVN(options, command, os.path.join(self._root_dir, self.relpath))
713
714 def diff(self, options, args, file_list):
715 # NOTE: This function does not currently modify file_list.
716 command = ['diff']
717 command.extend(args)
718 RunSVN(options, command, os.path.join(self._root_dir, self.relpath))
719
720 def update(self, options, args, file_list):
721 """Runs SCM to update or transparently checkout the working copy.
722
723 All updated files will be appended to file_list.
724
725 Raises:
726 Error: if can't get URL for relative path.
727 """
728 # Only update if git is not controlling the directory.
729 git_path = os.path.join(self._root_dir, self.relpath, '.git')
730 if options.path_exists(git_path):
731 print >> options.stdout, (
732 "________ found .git directory; skipping %s" % self.relpath)
733 return
734
735 if args:
736 raise Error("Unsupported argument(s): %s" % ",".join(args))
737
738 url = self.url
739 components = url.split("@")
740 revision = None
741 forced_revision = False
742 if options.revision:
743 # Override the revision number.
744 url = '%s@%s' % (components[0], str(options.revision))
745 revision = int(options.revision)
746 forced_revision = True
747 elif len(components) == 2:
748 revision = int(components[1])
749 forced_revision = True
750
751 rev_str = ""
752 if revision:
753 rev_str = ' at %d' % revision
754
755 if not options.path_exists(os.path.join(self._root_dir, self.relpath)):
756 # We need to checkout.
757 command = ['checkout', url, os.path.join(self._root_dir, self.relpath)]
758 RunSVNAndGetFileList(options, command, self._root_dir, file_list)
759
760 # Get the existing scm url and the revision number of the current checkout.
761 from_info = CaptureSVNInfo(options,
762 os.path.join(self._root_dir, self.relpath, '.'),
763 '.')
764
765 if options.manually_grab_svn_rev:
766 # Retrieve the current HEAD version because svn is slow at null updates.
767 if not revision:
768 from_info_live = CaptureSVNInfo(options, from_info.url, '.')
769 revision = int(from_info_live.revision)
770 rev_str = ' at %d' % revision
771
772 if from_info.url != components[0]:
773 to_info = CaptureSVNInfo(options, url, '.')
774 if from_info.root != to_info.root:
775 # We have different roots, so check if we can switch --relocate.
776 # Subversion only permits this if the repository UUIDs match.
777 if from_info.uuid != to_info.uuid:
778 raise Error("Can't switch the checkout to %s; UUID don't match" % url)
779
780 # Perform the switch --relocate, then rewrite the from_url
781 # to reflect where we "are now." (This is the same way that
782 # Subversion itself handles the metadata when switch --relocate
783 # is used.) This makes the checks below for whether we
784 # can update to a revision or have to switch to a different
785 # branch work as expected.
786 # TODO(maruel): TEST ME !
787 command = ["switch", "--relocate", from_info.root, to_info.root,
788 self.relpath]
789 RunSVN(options, command, self._root_dir)
790 from_info.url = from_info.url.replace(from_info.root, to_info.root)
791
792 # If the provided url has a revision number that matches the revision
793 # number of the existing directory, then we don't need to bother updating.
794 if not options.force and from_info.revision == revision:
795 if options.verbose or not forced_revision:
796 print >>options.stdout, ("\n_____ %s%s" % (
797 self.relpath, rev_str))
798 return
799
800 command = ["update", os.path.join(self._root_dir, self.relpath)]
801 if revision:
802 command.extend(['--revision', str(revision)])
803 RunSVNAndGetFileList(options, command, self._root_dir, file_list)
804
805 def revert(self, options, args, file_list):
806 """Reverts local modifications. Subversion specific.
807
808 All reverted files will be appended to file_list, even if Subversion
809 doesn't know about them.
810 """
811 path = os.path.join(self._root_dir, self.relpath)
812 if not os.path.isdir(path):
813 # We can't revert path that doesn't exist.
814 # TODO(maruel): Should we update instead?
815 if options.verbose:
816 print >>options.stdout, ("\n_____ %s is missing, can't revert" %
817 self.relpath)
818 return
819
820 files = CaptureSVNStatus(options, path)
821 # Batch the command.
822 files_to_revert = []
823 for file in files:
824 file_path = os.path.join(path, file.path)
825 print >>options.stdout, file_path
826 # Unversioned file or unexpected unversioned file.
827 if file.text_status in ('?', '~'):
828 # Remove extraneous file. Also remove unexpected unversioned
829 # directories. svn won't touch them but we want to delete these.
830 file_list.append(file_path)
831 try:
832 os.remove(file_path)
833 except EnvironmentError:
834 RemoveDirectory(file_path)
835
836 if file.text_status != '?':
837 # For any other status, svn revert will work.
838 file_list.append(file_path)
839 files_to_revert.append(file.path)
840
841 # Revert them all at once.
842 if files_to_revert:
843 accumulated_paths = []
844 accumulated_length = 0
845 command = ['revert']
846 for p in files_to_revert:
847 # Some shell have issues with command lines too long.
848 if accumulated_length and accumulated_length + len(p) > 3072:
849 RunSVN(options, command + accumulated_paths,
850 os.path.join(self._root_dir, self.relpath))
851 accumulated_paths = []
852 accumulated_length = 0
853 else:
854 accumulated_paths.append(p)
855 accumulated_length += len(p)
856 if accumulated_paths:
857 RunSVN(options, command + accumulated_paths,
858 os.path.join(self._root_dir, self.relpath))
859
860 def status(self, options, args, file_list):
861 """Display status information."""
862 command = ['status']
863 command.extend(args)
864 RunSVNAndGetFileList(options, command,
865 os.path.join(self._root_dir, self.relpath), file_list)
866
867
868## GClient implementation.
869
870
871class GClient(object):
872 """Object that represent a gclient checkout."""
873
874 supported_commands = [
875 'cleanup', 'diff', 'revert', 'status', 'update', 'runhooks'
876 ]
877
878 def __init__(self, root_dir, options):
879 self._root_dir = root_dir
880 self._options = options
881 self._config_content = None
882 self._config_dict = {}
883 self._deps_hooks = []
884
885 def SetConfig(self, content):
886 self._config_dict = {}
887 self._config_content = content
888 exec(content, self._config_dict)
889
890 def SaveConfig(self):
891 FileWrite(os.path.join(self._root_dir, self._options.config_filename),
892 self._config_content)
893
894 def _LoadConfig(self):
895 client_source = FileRead(os.path.join(self._root_dir,
896 self._options.config_filename))
897 self.SetConfig(client_source)
898
899 def ConfigContent(self):
900 return self._config_content
901
902 def GetVar(self, key, default=None):
903 return self._config_dict.get(key, default)
904
905 @staticmethod
906 def LoadCurrentConfig(options, from_dir=None):
907 """Searches for and loads a .gclient file relative to the current working
908 dir.
909
910 Returns:
911 A dict representing the contents of the .gclient file or an empty dict if
912 the .gclient file doesn't exist.
913 """
914 if not from_dir:
915 from_dir = os.curdir
916 path = os.path.realpath(from_dir)
917 while not options.path_exists(os.path.join(path, options.config_filename)):
918 next = os.path.split(path)
919 if not next[1]:
920 return None
921 path = next[0]
922 client = options.gclient(path, options)
923 client._LoadConfig()
924 return client
925
926 def SetDefaultConfig(self, solution_name, solution_url, safesync_url):
927 self.SetConfig(DEFAULT_CLIENT_FILE_TEXT % (
928 solution_name, solution_url, safesync_url
929 ))
930
931 def _SaveEntries(self, entries):
932 """Creates a .gclient_entries file to record the list of unique checkouts.
933
934 The .gclient_entries file lives in the same directory as .gclient.
935
936 Args:
937 entries: A sequence of solution names.
938 """
939 text = "entries = [\n"
940 for entry in entries:
941 text += " \"%s\",\n" % entry
942 text += "]\n"
943 FileWrite(os.path.join(self._root_dir, self._options.entries_filename),
944 text)
945
946 def _ReadEntries(self):
947 """Read the .gclient_entries file for the given client.
948
949 Args:
950 client: The client for which the entries file should be read.
951
952 Returns:
953 A sequence of solution names, which will be empty if there is the
954 entries file hasn't been created yet.
955 """
956 scope = {}
957 filename = os.path.join(self._root_dir, self._options.entries_filename)
958 if not self._options.path_exists(filename):
959 return []
960 exec(FileRead(filename), scope)
961 return scope["entries"]
962
963 class FromImpl:
964 """Used to implement the From syntax."""
965
966 def __init__(self, module_name):
967 self.module_name = module_name
968
969 def __str__(self):
970 return 'From("%s")' % self.module_name
971
972 class _VarImpl:
973 def __init__(self, custom_vars, local_scope):
974 self._custom_vars = custom_vars
975 self._local_scope = local_scope
976
977 def Lookup(self, var_name):
978 """Implements the Var syntax."""
979 if var_name in self._custom_vars:
980 return self._custom_vars[var_name]
981 elif var_name in self._local_scope.get("vars", {}):
982 return self._local_scope["vars"][var_name]
983 raise Error("Var is not defined: %s" % var_name)
984
985 def _ParseSolutionDeps(self, solution_name, solution_deps_content,
986 custom_vars):
987 """Parses the DEPS file for the specified solution.
988
989 Args:
990 solution_name: The name of the solution to query.
991 solution_deps_content: Content of the DEPS file for the solution
992 custom_vars: A dict of vars to override any vars defined in the DEPS file.
993
994 Returns:
995 A dict mapping module names (as relative paths) to URLs or an empty
996 dict if the solution does not have a DEPS file.
997 """
998 # Skip empty
999 if not solution_deps_content:
1000 return {}
1001 # Eval the content
1002 local_scope = {}
1003 var = self._VarImpl(custom_vars, local_scope)
1004 global_scope = {"From": self.FromImpl, "Var": var.Lookup, "deps_os": {}}
1005 exec(solution_deps_content, global_scope, local_scope)
1006 deps = local_scope.get("deps", {})
1007
1008 # load os specific dependencies if defined. these dependencies may
1009 # override or extend the values defined by the 'deps' member.
1010 if "deps_os" in local_scope:
1011 deps_os_choices = {
1012 "win32": "win",
1013 "win": "win",
1014 "cygwin": "win",
1015 "darwin": "mac",
1016 "mac": "mac",
1017 "unix": "unix",
1018 "linux": "unix",
1019 "linux2": "unix",
1020 }
1021
1022 if self._options.deps_os is not None:
1023 deps_to_include = self._options.deps_os.split(",")
1024 if "all" in deps_to_include:
1025 deps_to_include = deps_os_choices.values()
1026 else:
1027 deps_to_include = [deps_os_choices.get(self._options.platform, "unix")]
1028
1029 deps_to_include = set(deps_to_include)
1030 for deps_os_key in deps_to_include:
1031 os_deps = local_scope["deps_os"].get(deps_os_key, {})
1032 if len(deps_to_include) > 1:
1033 # Ignore any overrides when including deps for more than one
1034 # platform, so we collect the broadest set of dependencies available.
1035 # We may end up with the wrong revision of something for our
1036 # platform, but this is the best we can do.
1037 deps.update([x for x in os_deps.items() if not x[0] in deps])
1038 else:
1039 deps.update(os_deps)
1040
1041 if 'hooks' in local_scope:
1042 self._deps_hooks.extend(local_scope['hooks'])
1043
1044 # If use_relative_paths is set in the DEPS file, regenerate
1045 # the dictionary using paths relative to the directory containing
1046 # the DEPS file.
1047 if local_scope.get('use_relative_paths'):
1048 rel_deps = {}
1049 for d, url in deps.items():
1050 # normpath is required to allow DEPS to use .. in their
1051 # dependency local path.
1052 rel_deps[os.path.normpath(os.path.join(solution_name, d))] = url
1053 return rel_deps
1054 else:
1055 return deps
1056
1057 def _ParseAllDeps(self, solution_urls, solution_deps_content):
1058 """Parse the complete list of dependencies for the client.
1059
1060 Args:
1061 solution_urls: A dict mapping module names (as relative paths) to URLs
1062 corresponding to the solutions specified by the client. This parameter
1063 is passed as an optimization.
1064 solution_deps_content: A dict mapping module names to the content
1065 of their DEPS files
1066
1067 Returns:
1068 A dict mapping module names (as relative paths) to URLs corresponding
1069 to the entire set of dependencies to checkout for the given client.
1070
1071 Raises:
1072 Error: If a dependency conflicts with another dependency or of a solution.
1073 """
1074 deps = {}
1075 for solution in self.GetVar("solutions"):
1076 custom_vars = solution.get("custom_vars", {})
1077 solution_deps = self._ParseSolutionDeps(
1078 solution["name"],
1079 solution_deps_content[solution["name"]],
1080 custom_vars)
1081
1082 # If a line is in custom_deps, but not in the solution, we want to append
1083 # this line to the solution.
1084 if "custom_deps" in solution:
1085 for d in solution["custom_deps"]:
1086 if d not in solution_deps:
1087 solution_deps[d] = solution["custom_deps"][d]
1088
1089 for d in solution_deps:
1090 if "custom_deps" in solution and d in solution["custom_deps"]:
1091 # Dependency is overriden.
1092 url = solution["custom_deps"][d]
1093 if url is None:
1094 continue
1095 else:
1096 url = solution_deps[d]
1097 # if we have a From reference dependent on another solution, then
1098 # just skip the From reference. When we pull deps for the solution,
1099 # we will take care of this dependency.
1100 #
1101 # If multiple solutions all have the same From reference, then we
1102 # should only add one to our list of dependencies.
1103 if type(url) != str:
1104 if url.module_name in solution_urls:
1105 # Already parsed.
1106 continue
1107 if d in deps and type(deps[d]) != str:
1108 if url.module_name == deps[d].module_name:
1109 continue
1110 else:
1111 parsed_url = urlparse.urlparse(url)
1112 scheme = parsed_url[0]
1113 if not scheme:
1114 # A relative url. Fetch the real base.
1115 path = parsed_url[2]
1116 if path[0] != "/":
1117 raise Error(
1118 "relative DEPS entry \"%s\" must begin with a slash" % d)
1119 # Create a scm just to query the full url.
1120 scm = self._options.scm_wrapper(solution["url"], self._root_dir,
1121 None)
1122 url = scm.FullUrlForRelativeUrl(url)
1123 if d in deps and deps[d] != url:
1124 raise Error(
1125 "Solutions have conflicting versions of dependency \"%s\"" % d)
1126 if d in solution_urls and solution_urls[d] != url:
1127 raise Error(
1128 "Dependency \"%s\" conflicts with specified solution" % d)
1129 # Grab the dependency.
1130 deps[d] = url
1131 return deps
1132
1133 def _RunHookAction(self, hook_dict):
1134 """Runs the action from a single hook.
1135 """
1136 command = hook_dict['action'][:]
1137 if command[0] == 'python':
1138 # If the hook specified "python" as the first item, the action is a
1139 # Python script. Run it by starting a new copy of the same
1140 # interpreter.
1141 command[0] = sys.executable
1142
1143 # Use a discrete exit status code of 2 to indicate that a hook action
1144 # failed. Users of this script may wish to treat hook action failures
1145 # differently from VC failures.
1146 SubprocessCall(command, self._root_dir, self._options.stdout,
1147 fail_status=2)
1148
1149 def _RunHooks(self, command, file_list, is_using_git):
1150 """Evaluates all hooks, running actions as needed.
1151 """
1152 # Hooks only run for these command types.
1153 if not command in ('update', 'revert', 'runhooks'):
1154 return
1155
1156 # Get any hooks from the .gclient file.
1157 hooks = self.GetVar("hooks", [])
1158 # Add any hooks found in DEPS files.
1159 hooks.extend(self._deps_hooks)
1160
1161 # If "--force" was specified, run all hooks regardless of what files have
1162 # changed. If the user is using git, then we don't know what files have
1163 # changed so we always run all hooks.
1164 if self._options.force or is_using_git:
1165 for hook_dict in hooks:
1166 self._RunHookAction(hook_dict)
1167 return
1168
1169 # Run hooks on the basis of whether the files from the gclient operation
1170 # match each hook's pattern.
1171 for hook_dict in hooks:
1172 pattern = re.compile(hook_dict['pattern'])
1173 for file in file_list:
1174 if not pattern.search(file):
1175 continue
1176
1177 self._RunHookAction(hook_dict)
1178
1179 # The hook's action only runs once. Don't bother looking for any
1180 # more matches.
1181 break
1182
1183 def RunOnDeps(self, command, args):
1184 """Runs a command on each dependency in a client and its dependencies.
1185
1186 The module's dependencies are specified in its top-level DEPS files.
1187
1188 Args:
1189 command: The command to use (e.g., 'status' or 'diff')
1190 args: list of str - extra arguments to add to the command line.
1191
1192 Raises:
1193 Error: If the client has conflicting entries.
1194 """
1195 if not command in self.supported_commands:
1196 raise Error("'%s' is an unsupported command" % command)
1197
1198 # Check for revision overrides.
1199 revision_overrides = {}
1200 for revision in self._options.revisions:
1201 if revision.find("@") == -1:
1202 raise Error(
1203 "Specify the full dependency when specifying a revision number.")
1204 revision_elem = revision.split("@")
1205 # Disallow conflicting revs
1206 if revision_overrides.has_key(revision_elem[0]) and \
1207 revision_overrides[revision_elem[0]] != revision_elem[1]:
1208 raise Error(
1209 "Conflicting revision numbers specified.")
1210 revision_overrides[revision_elem[0]] = revision_elem[1]
1211
1212 solutions = self.GetVar("solutions")
1213 if not solutions:
1214 raise Error("No solution specified")
1215
1216 # When running runhooks --force, there's no need to consult the SCM.
1217 # All known hooks are expected to run unconditionally regardless of working
1218 # copy state, so skip the SCM status check.
1219 run_scm = not (command == 'runhooks' and self._options.force)
1220
1221 entries = {}
1222 entries_deps_content = {}
1223 file_list = []
1224 # Run on the base solutions first.
1225 for solution in solutions:
1226 name = solution["name"]
1227 if name in entries:
1228 raise Error("solution %s specified more than once" % name)
1229 url = solution["url"]
1230 entries[name] = url
1231 if run_scm:
1232 self._options.revision = revision_overrides.get(name)
1233 scm = self._options.scm_wrapper(url, self._root_dir, name)
1234 scm.RunCommand(command, self._options, args, file_list)
1235 self._options.revision = None
1236 try:
1237 deps_content = FileRead(os.path.join(self._root_dir, name,
1238 self._options.deps_file))
1239 except IOError, e:
1240 if e.errno != errno.ENOENT:
1241 raise
1242 deps_content = ""
1243 entries_deps_content[name] = deps_content
1244
1245 # Process the dependencies next (sort alphanumerically to ensure that
1246 # containing directories get populated first and for readability)
1247 deps = self._ParseAllDeps(entries, entries_deps_content)
1248 deps_to_process = deps.keys()
1249 deps_to_process.sort()
1250
1251 # First pass for direct dependencies.
1252 for d in deps_to_process:
1253 if type(deps[d]) == str:
1254 url = deps[d]
1255 entries[d] = url
1256 if run_scm:
1257 self._options.revision = revision_overrides.get(d)
1258 scm = self._options.scm_wrapper(url, self._root_dir, d)
1259 scm.RunCommand(command, self._options, args, file_list)
1260 self._options.revision = None
1261
1262 # Second pass for inherited deps (via the From keyword)
1263 for d in deps_to_process:
1264 if type(deps[d]) != str:
1265 sub_deps = self._ParseSolutionDeps(
1266 deps[d].module_name,
1267 FileRead(os.path.join(self._root_dir,
1268 deps[d].module_name,
1269 self._options.deps_file)),
1270 {})
1271 url = sub_deps[d]
1272 entries[d] = url
1273 if run_scm:
1274 self._options.revision = revision_overrides.get(d)
1275 scm = self._options.scm_wrapper(url, self._root_dir, d)
1276 scm.RunCommand(command, self._options, args, file_list)
1277 self._options.revision = None
1278
1279 is_using_git = IsUsingGit(self._root_dir, entries.keys())
1280 self._RunHooks(command, file_list, is_using_git)
1281
1282 if command == 'update':
1283 # notify the user if there is an orphaned entry in their working copy.
1284 # TODO(darin): we should delete this directory manually if it doesn't
1285 # have any changes in it.
1286 prev_entries = self._ReadEntries()
1287 for entry in prev_entries:
1288 e_dir = os.path.join(self._root_dir, entry)
1289 if entry not in entries and self._options.path_exists(e_dir):
1290 if CaptureSVNStatus(self._options, e_dir):
1291 # There are modified files in this entry
1292 entries[entry] = None # Keep warning until removed.
1293 print >> self._options.stdout, (
1294 "\nWARNING: \"%s\" is no longer part of this client. "
1295 "It is recommended that you manually remove it.\n") % entry
1296 else:
1297 # Delete the entry
1298 print >> self._options.stdout, ("\n________ deleting \'%s\' " +
1299 "in \'%s\'") % (entry, self._root_dir)
1300 RemoveDirectory(e_dir)
1301 # record the current list of entries for next time
1302 self._SaveEntries(entries)
1303
1304 def PrintRevInfo(self):
1305 """Output revision info mapping for the client and its dependencies. This
1306 allows the capture of a overall "revision" for the source tree that can
1307 be used to reproduce the same tree in the future. The actual output
1308 contains enough information (source paths, svn server urls and revisions)
1309 that it can be used either to generate external svn commands (without
1310 gclient) or as input to gclient's --rev option (with some massaging of
1311 the data).
1312
1313 NOTE: Unlike RunOnDeps this does not require a local checkout and is run
1314 on the Pulse master. It MUST NOT execute hooks.
1315
1316 Raises:
1317 Error: If the client has conflicting entries.
1318 """
1319 # Check for revision overrides.
1320 revision_overrides = {}
1321 for revision in self._options.revisions:
1322 if revision.find("@") < 0:
1323 raise Error(
1324 "Specify the full dependency when specifying a revision number.")
1325 revision_elem = revision.split("@")
1326 # Disallow conflicting revs
1327 if revision_overrides.has_key(revision_elem[0]) and \
1328 revision_overrides[revision_elem[0]] != revision_elem[1]:
1329 raise Error(
1330 "Conflicting revision numbers specified.")
1331 revision_overrides[revision_elem[0]] = revision_elem[1]
1332
1333 solutions = self.GetVar("solutions")
1334 if not solutions:
1335 raise Error("No solution specified")
1336
1337 entries = {}
1338 entries_deps_content = {}
1339
1340 # Inner helper to generate base url and rev tuple (including honoring
1341 # |revision_overrides|)
1342 def GetURLAndRev(name, original_url):
1343 if original_url.find("@") < 0:
1344 if revision_overrides.has_key(name):
1345 return (original_url, int(revision_overrides[name]))
1346 else:
1347 # TODO(aharper): SVN/SCMWrapper cleanup (non-local commandset)
1348 return (original_url, CaptureSVNHeadRevision(self._options,
1349 original_url))
1350 else:
1351 url_components = original_url.split("@")
1352 if revision_overrides.has_key(name):
1353 return (url_components[0], int(revision_overrides[name]))
1354 else:
1355 return (url_components[0], int(url_components[1]))
1356
1357 # Run on the base solutions first.
1358 for solution in solutions:
1359 name = solution["name"]
1360 if name in entries:
1361 raise Error("solution %s specified more than once" % name)
1362 (url, rev) = GetURLAndRev(name, solution["url"])
1363 entries[name] = "%s@%d" % (url, rev)
1364 # TODO(aharper): SVN/SCMWrapper cleanup (non-local commandset)
1365 entries_deps_content[name] = CaptureSVN(
1366 self._options,
1367 ["cat",
1368 "%s/%s@%d" % (url,
1369 self._options.deps_file,
1370 rev)],
1371 os.getcwd())
1372
1373 # Process the dependencies next (sort alphanumerically to ensure that
1374 # containing directories get populated first and for readability)
1375 deps = self._ParseAllDeps(entries, entries_deps_content)
1376 deps_to_process = deps.keys()
1377 deps_to_process.sort()
1378
1379 # First pass for direct dependencies.
1380 for d in deps_to_process:
1381 if type(deps[d]) == str:
1382 (url, rev) = GetURLAndRev(d, deps[d])
1383 entries[d] = "%s@%d" % (url, rev)
1384
1385 # Second pass for inherited deps (via the From keyword)
1386 for d in deps_to_process:
1387 if type(deps[d]) != str:
1388 deps_parent_url = entries[deps[d].module_name]
1389 if deps_parent_url.find("@") < 0:
1390 raise Error("From %s missing revisioned url" % deps[d].module_name)
1391 deps_parent_url_components = deps_parent_url.split("@")
1392 # TODO(aharper): SVN/SCMWrapper cleanup (non-local commandset)
1393 deps_parent_content = CaptureSVN(
1394 self._options,
1395 ["cat",
1396 "%s/%s@%s" % (deps_parent_url_components[0],
1397 self._options.deps_file,
1398 deps_parent_url_components[1])],
1399 os.getcwd())
1400 sub_deps = self._ParseSolutionDeps(
1401 deps[d].module_name,
1402 FileRead(os.path.join(self._root_dir,
1403 deps[d].module_name,
1404 self._options.deps_file)),
1405 {})
1406 (url, rev) = GetURLAndRev(d, sub_deps[d])
1407 entries[d] = "%s@%d" % (url, rev)
1408
1409 print ";".join(["%s,%s" % (x, entries[x]) for x in sorted(entries.keys())])
1410
1411
1412## gclient commands.
1413
1414
1415def DoCleanup(options, args):
1416 """Handle the cleanup subcommand.
1417
1418 Raises:
1419 Error: if client isn't configured properly.
1420 """
1421 client = options.gclient.LoadCurrentConfig(options)
1422 if not client:
1423 raise Error("client not configured; see 'gclient config'")
1424 if options.verbose:
1425 # Print out the .gclient file. This is longer than if we just printed the
1426 # client dict, but more legible, and it might contain helpful comments.
1427 print >>options.stdout, client.ConfigContent()
1428 options.verbose = True
1429 return client.RunOnDeps('cleanup', args)
1430
1431
1432def DoConfig(options, args):
1433 """Handle the config subcommand.
1434
1435 Args:
1436 options: If options.spec set, a string providing contents of config file.
1437 args: The command line args. If spec is not set,
1438 then args[0] is a string URL to get for config file.
1439
1440 Raises:
1441 Error: on usage error
1442 """
1443 if len(args) < 1 and not options.spec:
1444 raise Error("required argument missing; see 'gclient help config'")
1445 if options.path_exists(options.config_filename):
1446 raise Error("%s file already exists in the current directory" %
1447 options.config_filename)
1448 client = options.gclient('.', options)
1449 if options.spec:
1450 client.SetConfig(options.spec)
1451 else:
1452 # TODO(darin): it would be nice to be able to specify an alternate relpath
1453 # for the given URL.
1454 base_url = args[0]
1455 name = args[0].split("/")[-1]
1456 safesync_url = ""
1457 if len(args) > 1:
1458 safesync_url = args[1]
1459 client.SetDefaultConfig(name, base_url, safesync_url)
1460 client.SaveConfig()
1461
1462
1463def DoHelp(options, args):
1464 """Handle the help subcommand giving help for another subcommand.
1465
1466 Raises:
1467 Error: if the command is unknown.
1468 """
1469 if len(args) == 1 and args[0] in COMMAND_USAGE_TEXT:
1470 print >>options.stdout, COMMAND_USAGE_TEXT[args[0]]
1471 else:
1472 raise Error("unknown subcommand '%s'; see 'gclient help'" % args[0])
1473
1474
1475def DoStatus(options, args):
1476 """Handle the status subcommand.
1477
1478 Raises:
1479 Error: if client isn't configured properly.
1480 """
1481 client = options.gclient.LoadCurrentConfig(options)
1482 if not client:
1483 raise Error("client not configured; see 'gclient config'")
1484 if options.verbose:
1485 # Print out the .gclient file. This is longer than if we just printed the
1486 # client dict, but more legible, and it might contain helpful comments.
1487 print >>options.stdout, client.ConfigContent()
1488 options.verbose = True
1489 return client.RunOnDeps('status', args)
1490
1491
1492def DoUpdate(options, args):
1493 """Handle the update and sync subcommands.
1494
1495 Raises:
1496 Error: if client isn't configured properly.
1497 """
1498 client = options.gclient.LoadCurrentConfig(options)
1499
1500 if not client:
1501 raise Error("client not configured; see 'gclient config'")
1502
1503 if not options.head:
1504 solutions = client.GetVar('solutions')
1505 if solutions:
1506 for s in solutions:
1507 if s.get('safesync_url', ''):
1508 # rip through revisions and make sure we're not over-riding
1509 # something that was explicitly passed
1510 has_key = False
1511 for r in options.revisions:
1512 if r.split('@')[0] == s['name']:
1513 has_key = True
1514 break
1515
1516 if not has_key:
1517 handle = urllib.urlopen(s['safesync_url'])
1518 rev = handle.read().strip()
1519 handle.close()
1520 if len(rev):
1521 options.revisions.append(s['name']+'@'+rev)
1522
1523 if options.verbose:
1524 # Print out the .gclient file. This is longer than if we just printed the
1525 # client dict, but more legible, and it might contain helpful comments.
1526 print >>options.stdout, client.ConfigContent()
1527 return client.RunOnDeps('update', args)
1528
1529
1530def DoDiff(options, args):
1531 """Handle the diff subcommand.
1532
1533 Raises:
1534 Error: if client isn't configured properly.
1535 """
1536 client = options.gclient.LoadCurrentConfig(options)
1537 if not client:
1538 raise Error("client not configured; see 'gclient config'")
1539 if options.verbose:
1540 # Print out the .gclient file. This is longer than if we just printed the
1541 # client dict, but more legible, and it might contain helpful comments.
1542 print >>options.stdout, client.ConfigContent()
1543 options.verbose = True
1544 return client.RunOnDeps('diff', args)
1545
1546
1547def DoRevert(options, args):
1548 """Handle the revert subcommand.
1549
1550 Raises:
1551 Error: if client isn't configured properly.
1552 """
1553 client = options.gclient.LoadCurrentConfig(options)
1554 if not client:
1555 raise Error("client not configured; see 'gclient config'")
1556 return client.RunOnDeps('revert', args)
1557
1558
1559def DoRunHooks(options, args):
1560 """Handle the runhooks subcommand.
1561
1562 Raises:
1563 Error: if client isn't configured properly.
1564 """
1565 client = options.gclient.LoadCurrentConfig(options)
1566 if not client:
1567 raise Error("client not configured; see 'gclient config'")
1568 if options.verbose:
1569 # Print out the .gclient file. This is longer than if we just printed the
1570 # client dict, but more legible, and it might contain helpful comments.
1571 print >>options.stdout, client.ConfigContent()
1572 return client.RunOnDeps('runhooks', args)
1573
1574
1575def DoRevInfo(options, args):
1576 """Handle the revinfo subcommand.
1577
1578 Raises:
1579 Error: if client isn't configured properly.
1580 """
1581 client = options.gclient.LoadCurrentConfig(options)
1582 if not client:
1583 raise Error("client not configured; see 'gclient config'")
1584 client.PrintRevInfo()
1585
1586
1587gclient_command_map = {
1588 "cleanup": DoCleanup,
1589 "config": DoConfig,
1590 "diff": DoDiff,
1591 "help": DoHelp,
1592 "status": DoStatus,
1593 "sync": DoUpdate,
1594 "update": DoUpdate,
1595 "revert": DoRevert,
1596 "runhooks": DoRunHooks,
1597 "revinfo" : DoRevInfo,
1598}
1599
1600
1601def DispatchCommand(command, options, args, command_map=None):
1602 """Dispatches the appropriate subcommand based on command line arguments."""
1603 if command_map is None:
1604 command_map = gclient_command_map
1605
1606 if command in command_map:
1607 return command_map[command](options, args)
1608 else:
1609 raise Error("unknown subcommand '%s'; see 'gclient help'" % command)
1610
1611
1612def Main(argv):
1613 """Parse command line arguments and dispatch command."""
1614
1615 option_parser = optparse.OptionParser(usage=DEFAULT_USAGE_TEXT,
1616 version=__version__)
1617 option_parser.disable_interspersed_args()
1618 option_parser.add_option("", "--force", action="store_true", default=False,
1619 help=("(update/sync only) force update even "
1620 "for modules which haven't changed"))
1621 option_parser.add_option("", "--revision", action="append", dest="revisions",
1622 metavar="REV", default=[],
1623 help=("(update/sync only) sync to a specific "
1624 "revision, can be used multiple times for "
1625 "each solution, e.g. --revision=src@123, "
1626 "--revision=internal@32"))
1627 option_parser.add_option("", "--deps", default=None, dest="deps_os",
1628 metavar="OS_LIST",
1629 help=("(update/sync only) sync deps for the "
1630 "specified (comma-separated) platform(s); "
1631 "'all' will sync all platforms"))
1632 option_parser.add_option("", "--spec", default=None,
1633 help=("(config only) create a gclient file "
1634 "containing the provided string"))
1635 option_parser.add_option("", "--verbose", action="store_true", default=False,
1636 help="produce additional output for diagnostics")
1637 option_parser.add_option("", "--manually_grab_svn_rev", action="store_true",
1638 default=False,
1639 help="Skip svn up whenever possible by requesting "
1640 "actual HEAD revision from the repository")
1641 option_parser.add_option("", "--head", action="store_true", default=False,
1642 help=("skips any safesync_urls specified in "
1643 "configured solutions"))
1644
1645 if len(argv) < 2:
1646 # Users don't need to be told to use the 'help' command.
1647 option_parser.print_help()
1648 return 1
1649 # Add manual support for --version as first argument.
1650 if argv[1] == '--version':
1651 option_parser.print_version()
1652 return 0
1653
1654 # Add manual support for --help as first argument.
1655 if argv[1] == '--help':
1656 argv[1] = 'help'
1657
1658 command = argv[1]
1659 options, args = option_parser.parse_args(argv[2:])
1660
1661 if len(argv) < 3 and command == "help":
1662 option_parser.print_help()
1663 return 0
1664
1665 # Files used for configuration and state saving.
1666 options.config_filename = os.environ.get("GCLIENT_FILE", ".gclient")
1667 options.entries_filename = ".gclient_entries"
1668 options.deps_file = "DEPS"
1669
1670 # These are overridded when testing. They are not externally visible.
1671 options.stdout = sys.stdout
1672 options.path_exists = os.path.exists
1673 options.gclient = GClient
1674 options.scm_wrapper = SCMWrapper
1675 options.platform = sys.platform
1676 return DispatchCommand(command, options, args)
1677
1678
1679if "__main__" == __name__:
1680 try:
1681 result = Main(sys.argv)
1682 except Error, e:
1683 print "Error: %s" % str(e)
1684 result = 1
1685 sys.exit(result)
1686
1687# vim: ts=2:sw=2:tw=80:et: