blob: 769c3061c8d0f740ba6c0766470094cdb41cc84c [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.orgedd27d12009-05-01 17:46:56 +0000595 def __init__(self, path, text_status, props, lock, history):
maruel@chromium.orge105d8d2009-04-30 17:58:25 +0000596 self.path = path
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000597 self.text_status = text_status
598 self.props = props
maruel@chromium.orgedd27d12009-05-01 17:46:56 +0000599 self.lock = lock
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000600 self.history = history
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000601
602 def __str__(self):
maruel@chromium.orge105d8d2009-04-30 17:58:25 +0000603 # Emulate svn status 1.5 output.
maruel@chromium.orgedd27d12009-05-01 17:46:56 +0000604 return (self.text_status + self.props + self.lock + self.history + ' ' +
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000605 self.path)
606
607
608def CaptureSVNStatus(options, path):
609 """Runs 'svn status' on an existing path.
610
611 Args:
612 path: The directory to run svn status.
613
614 Returns:
maruel@chromium.orge105d8d2009-04-30 17:58:25 +0000615 An array of FileStatus corresponding to the emulated output of 'svn status'
616 version 1.5."""
617 dom = ParseXML(CaptureSVN(options, ["status", "--xml"], path))
618 results = []
619 if dom:
620 # /status/target/entry/(wc-status|commit|author|date)
621 for target in dom.getElementsByTagName('target'):
622 base_path = target.getAttribute('path')
623 for entry in target.getElementsByTagName('entry'):
624 file = entry.getAttribute('path')
625 wc_status = entry.getElementsByTagName('wc-status')
626 assert len(wc_status) == 1
627 # Emulate svn 1.5 status ouput...
628 statuses = [' ' for i in range(7)]
629 # Col 0
630 xml_item_status = wc_status[0].getAttribute('item')
631 if xml_item_status == 'unversioned':
632 statuses[0] = '?'
633 elif xml_item_status == 'modified':
634 statuses[0] = 'M'
635 elif xml_item_status == 'added':
636 statuses[0] = 'A'
637 elif xml_item_status == 'conflicted':
638 statuses[0] = 'C'
maruel@chromium.orgedd27d12009-05-01 17:46:56 +0000639 elif xml_item_status in ('incomplete', 'missing'):
640 statuses[0] = '!'
maruel@chromium.orge105d8d2009-04-30 17:58:25 +0000641 elif not xml_item_status:
642 pass
643 else:
644 raise Exception('Unknown item status "%s"; please implement me!' %
645 xml_item_status)
646 # Col 1
647 xml_props_status = wc_status[0].getAttribute('props')
648 if xml_props_status == 'modified':
649 statuses[1] = 'M'
650 elif xml_props_status == 'conflicted':
651 statuses[1] = 'C'
652 elif (not xml_props_status or xml_props_status == 'none' or
653 xml_props_status == 'normal'):
654 pass
655 else:
656 raise Exception('Unknown props status "%s"; please implement me!' %
657 xml_props_status)
maruel@chromium.orgedd27d12009-05-01 17:46:56 +0000658 # Col 2
659 if wc_status[0].getAttribute('wc-locked') == 'true':
660 statuses[2] = 'L'
maruel@chromium.orge105d8d2009-04-30 17:58:25 +0000661 # Col 3
662 if wc_status[0].getAttribute('copied') == 'true':
663 statuses[3] = '+'
maruel@chromium.orgedd27d12009-05-01 17:46:56 +0000664 item = FileStatus(file, statuses[0], statuses[1], statuses[2],
665 statuses[3])
maruel@chromium.orge105d8d2009-04-30 17:58:25 +0000666 results.append(item)
667 return results
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000668
669
670### SCM abstraction layer
671
672
673class SCMWrapper(object):
674 """Add necessary glue between all the supported SCM.
675
676 This is the abstraction layer to bind to different SCM. Since currently only
677 subversion is supported, a lot of subersionism remains. This can be sorted out
678 once another SCM is supported."""
679 def __init__(self, url=None, root_dir=None, relpath=None,
680 scm_name='svn'):
681 # TODO(maruel): Deduce the SCM from the url.
682 self.scm_name = scm_name
683 self.url = url
684 self._root_dir = root_dir
685 if self._root_dir:
maruel@chromium.orge105d8d2009-04-30 17:58:25 +0000686 self._root_dir = self._root_dir.replace('/', os.sep)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000687 self.relpath = relpath
688 if self.relpath:
maruel@chromium.orge105d8d2009-04-30 17:58:25 +0000689 self.relpath = self.relpath.replace('/', os.sep)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000690
691 def FullUrlForRelativeUrl(self, url):
692 # Find the forth '/' and strip from there. A bit hackish.
693 return '/'.join(self.url.split('/')[:4]) + url
694
695 def RunCommand(self, command, options, args, file_list=None):
696 # file_list will have all files that are modified appended to it.
697
698 if file_list == None:
699 file_list = []
700
701 commands = {
702 'cleanup': self.cleanup,
703 'update': self.update,
704 'revert': self.revert,
705 'status': self.status,
706 'diff': self.diff,
707 'runhooks': self.status,
708 }
709
710 if not command in commands:
711 raise Error('Unknown command %s' % command)
712
713 return commands[command](options, args, file_list)
714
715 def cleanup(self, options, args, file_list):
716 """Cleanup working copy."""
717 command = ['cleanup']
718 command.extend(args)
719 RunSVN(options, command, os.path.join(self._root_dir, self.relpath))
720
721 def diff(self, options, args, file_list):
722 # NOTE: This function does not currently modify file_list.
723 command = ['diff']
724 command.extend(args)
725 RunSVN(options, command, os.path.join(self._root_dir, self.relpath))
726
727 def update(self, options, args, file_list):
728 """Runs SCM to update or transparently checkout the working copy.
729
730 All updated files will be appended to file_list.
731
732 Raises:
733 Error: if can't get URL for relative path.
734 """
735 # Only update if git is not controlling the directory.
736 git_path = os.path.join(self._root_dir, self.relpath, '.git')
737 if options.path_exists(git_path):
738 print >> options.stdout, (
739 "________ found .git directory; skipping %s" % self.relpath)
740 return
741
742 if args:
743 raise Error("Unsupported argument(s): %s" % ",".join(args))
744
745 url = self.url
746 components = url.split("@")
747 revision = None
748 forced_revision = False
749 if options.revision:
750 # Override the revision number.
751 url = '%s@%s' % (components[0], str(options.revision))
752 revision = int(options.revision)
753 forced_revision = True
754 elif len(components) == 2:
755 revision = int(components[1])
756 forced_revision = True
757
758 rev_str = ""
759 if revision:
760 rev_str = ' at %d' % revision
761
762 if not options.path_exists(os.path.join(self._root_dir, self.relpath)):
763 # We need to checkout.
764 command = ['checkout', url, os.path.join(self._root_dir, self.relpath)]
maruel@chromium.orgedd27d12009-05-01 17:46:56 +0000765 if revision:
766 command.extend(['--revision', str(revision)])
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000767 RunSVNAndGetFileList(options, command, self._root_dir, file_list)
maruel@chromium.orgedd27d12009-05-01 17:46:56 +0000768 return
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000769
770 # Get the existing scm url and the revision number of the current checkout.
771 from_info = CaptureSVNInfo(options,
772 os.path.join(self._root_dir, self.relpath, '.'),
773 '.')
774
775 if options.manually_grab_svn_rev:
776 # Retrieve the current HEAD version because svn is slow at null updates.
777 if not revision:
778 from_info_live = CaptureSVNInfo(options, from_info.url, '.')
779 revision = int(from_info_live.revision)
780 rev_str = ' at %d' % revision
781
782 if from_info.url != components[0]:
783 to_info = CaptureSVNInfo(options, url, '.')
784 if from_info.root != to_info.root:
785 # We have different roots, so check if we can switch --relocate.
786 # Subversion only permits this if the repository UUIDs match.
787 if from_info.uuid != to_info.uuid:
788 raise Error("Can't switch the checkout to %s; UUID don't match" % url)
789
790 # Perform the switch --relocate, then rewrite the from_url
791 # to reflect where we "are now." (This is the same way that
792 # Subversion itself handles the metadata when switch --relocate
793 # is used.) This makes the checks below for whether we
794 # can update to a revision or have to switch to a different
795 # branch work as expected.
796 # TODO(maruel): TEST ME !
797 command = ["switch", "--relocate", from_info.root, to_info.root,
798 self.relpath]
799 RunSVN(options, command, self._root_dir)
800 from_info.url = from_info.url.replace(from_info.root, to_info.root)
801
802 # If the provided url has a revision number that matches the revision
803 # number of the existing directory, then we don't need to bother updating.
804 if not options.force and from_info.revision == revision:
805 if options.verbose or not forced_revision:
806 print >>options.stdout, ("\n_____ %s%s" % (
807 self.relpath, rev_str))
808 return
809
810 command = ["update", os.path.join(self._root_dir, self.relpath)]
811 if revision:
812 command.extend(['--revision', str(revision)])
813 RunSVNAndGetFileList(options, command, self._root_dir, file_list)
814
815 def revert(self, options, args, file_list):
816 """Reverts local modifications. Subversion specific.
817
818 All reverted files will be appended to file_list, even if Subversion
819 doesn't know about them.
820 """
821 path = os.path.join(self._root_dir, self.relpath)
822 if not os.path.isdir(path):
maruel@chromium.orgedd27d12009-05-01 17:46:56 +0000823 # svn revert won't work if the directory doesn't exist. It needs to
824 # checkout instead.
825 print >>options.stdout, ("\n_____ %s is missing, synching instead" %
826 self.relpath)
827 # Don't reuse the args.
828 return self.update(options, [], file_list)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000829
830 files = CaptureSVNStatus(options, path)
831 # Batch the command.
832 files_to_revert = []
833 for file in files:
834 file_path = os.path.join(path, file.path)
835 print >>options.stdout, file_path
836 # Unversioned file or unexpected unversioned file.
837 if file.text_status in ('?', '~'):
838 # Remove extraneous file. Also remove unexpected unversioned
839 # directories. svn won't touch them but we want to delete these.
840 file_list.append(file_path)
841 try:
842 os.remove(file_path)
843 except EnvironmentError:
844 RemoveDirectory(file_path)
845
846 if file.text_status != '?':
847 # For any other status, svn revert will work.
848 file_list.append(file_path)
849 files_to_revert.append(file.path)
850
851 # Revert them all at once.
852 if files_to_revert:
853 accumulated_paths = []
854 accumulated_length = 0
855 command = ['revert']
856 for p in files_to_revert:
857 # Some shell have issues with command lines too long.
858 if accumulated_length and accumulated_length + len(p) > 3072:
859 RunSVN(options, command + accumulated_paths,
860 os.path.join(self._root_dir, self.relpath))
861 accumulated_paths = []
862 accumulated_length = 0
863 else:
864 accumulated_paths.append(p)
865 accumulated_length += len(p)
866 if accumulated_paths:
867 RunSVN(options, command + accumulated_paths,
868 os.path.join(self._root_dir, self.relpath))
869
870 def status(self, options, args, file_list):
871 """Display status information."""
maruel@chromium.orgedd27d12009-05-01 17:46:56 +0000872 path = os.path.join(self._root_dir, self.relpath)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000873 command = ['status']
874 command.extend(args)
maruel@chromium.orgedd27d12009-05-01 17:46:56 +0000875 if not os.path.isdir(path):
876 # svn status won't work if the directory doesn't exist.
877 print >> options.stdout, (
878 "\n________ couldn't run \'%s\' in \'%s\':\nThe directory "
879 "does not exist."
880 % (' '.join(command), path))
881 # There's no file list to retrieve.
882 else:
883 RunSVNAndGetFileList(options, command, path, file_list)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000884
885
886## GClient implementation.
887
888
889class GClient(object):
890 """Object that represent a gclient checkout."""
891
892 supported_commands = [
893 'cleanup', 'diff', 'revert', 'status', 'update', 'runhooks'
894 ]
895
896 def __init__(self, root_dir, options):
897 self._root_dir = root_dir
898 self._options = options
899 self._config_content = None
900 self._config_dict = {}
901 self._deps_hooks = []
902
903 def SetConfig(self, content):
904 self._config_dict = {}
905 self._config_content = content
906 exec(content, self._config_dict)
907
908 def SaveConfig(self):
909 FileWrite(os.path.join(self._root_dir, self._options.config_filename),
910 self._config_content)
911
912 def _LoadConfig(self):
913 client_source = FileRead(os.path.join(self._root_dir,
914 self._options.config_filename))
915 self.SetConfig(client_source)
916
917 def ConfigContent(self):
918 return self._config_content
919
920 def GetVar(self, key, default=None):
921 return self._config_dict.get(key, default)
922
923 @staticmethod
924 def LoadCurrentConfig(options, from_dir=None):
925 """Searches for and loads a .gclient file relative to the current working
926 dir.
927
928 Returns:
929 A dict representing the contents of the .gclient file or an empty dict if
930 the .gclient file doesn't exist.
931 """
932 if not from_dir:
933 from_dir = os.curdir
934 path = os.path.realpath(from_dir)
935 while not options.path_exists(os.path.join(path, options.config_filename)):
936 next = os.path.split(path)
937 if not next[1]:
938 return None
939 path = next[0]
940 client = options.gclient(path, options)
941 client._LoadConfig()
942 return client
943
944 def SetDefaultConfig(self, solution_name, solution_url, safesync_url):
945 self.SetConfig(DEFAULT_CLIENT_FILE_TEXT % (
946 solution_name, solution_url, safesync_url
947 ))
948
949 def _SaveEntries(self, entries):
950 """Creates a .gclient_entries file to record the list of unique checkouts.
951
952 The .gclient_entries file lives in the same directory as .gclient.
953
954 Args:
955 entries: A sequence of solution names.
956 """
957 text = "entries = [\n"
958 for entry in entries:
959 text += " \"%s\",\n" % entry
960 text += "]\n"
961 FileWrite(os.path.join(self._root_dir, self._options.entries_filename),
962 text)
963
964 def _ReadEntries(self):
965 """Read the .gclient_entries file for the given client.
966
967 Args:
968 client: The client for which the entries file should be read.
969
970 Returns:
971 A sequence of solution names, which will be empty if there is the
972 entries file hasn't been created yet.
973 """
974 scope = {}
975 filename = os.path.join(self._root_dir, self._options.entries_filename)
976 if not self._options.path_exists(filename):
977 return []
978 exec(FileRead(filename), scope)
979 return scope["entries"]
980
981 class FromImpl:
982 """Used to implement the From syntax."""
983
984 def __init__(self, module_name):
985 self.module_name = module_name
986
987 def __str__(self):
988 return 'From("%s")' % self.module_name
989
990 class _VarImpl:
991 def __init__(self, custom_vars, local_scope):
992 self._custom_vars = custom_vars
993 self._local_scope = local_scope
994
995 def Lookup(self, var_name):
996 """Implements the Var syntax."""
997 if var_name in self._custom_vars:
998 return self._custom_vars[var_name]
999 elif var_name in self._local_scope.get("vars", {}):
1000 return self._local_scope["vars"][var_name]
1001 raise Error("Var is not defined: %s" % var_name)
1002
1003 def _ParseSolutionDeps(self, solution_name, solution_deps_content,
1004 custom_vars):
1005 """Parses the DEPS file for the specified solution.
1006
1007 Args:
1008 solution_name: The name of the solution to query.
1009 solution_deps_content: Content of the DEPS file for the solution
1010 custom_vars: A dict of vars to override any vars defined in the DEPS file.
1011
1012 Returns:
1013 A dict mapping module names (as relative paths) to URLs or an empty
1014 dict if the solution does not have a DEPS file.
1015 """
1016 # Skip empty
1017 if not solution_deps_content:
1018 return {}
1019 # Eval the content
1020 local_scope = {}
1021 var = self._VarImpl(custom_vars, local_scope)
1022 global_scope = {"From": self.FromImpl, "Var": var.Lookup, "deps_os": {}}
1023 exec(solution_deps_content, global_scope, local_scope)
1024 deps = local_scope.get("deps", {})
1025
1026 # load os specific dependencies if defined. these dependencies may
1027 # override or extend the values defined by the 'deps' member.
1028 if "deps_os" in local_scope:
1029 deps_os_choices = {
1030 "win32": "win",
1031 "win": "win",
1032 "cygwin": "win",
1033 "darwin": "mac",
1034 "mac": "mac",
1035 "unix": "unix",
1036 "linux": "unix",
1037 "linux2": "unix",
1038 }
1039
1040 if self._options.deps_os is not None:
1041 deps_to_include = self._options.deps_os.split(",")
1042 if "all" in deps_to_include:
1043 deps_to_include = deps_os_choices.values()
1044 else:
1045 deps_to_include = [deps_os_choices.get(self._options.platform, "unix")]
1046
1047 deps_to_include = set(deps_to_include)
1048 for deps_os_key in deps_to_include:
1049 os_deps = local_scope["deps_os"].get(deps_os_key, {})
1050 if len(deps_to_include) > 1:
1051 # Ignore any overrides when including deps for more than one
1052 # platform, so we collect the broadest set of dependencies available.
1053 # We may end up with the wrong revision of something for our
1054 # platform, but this is the best we can do.
1055 deps.update([x for x in os_deps.items() if not x[0] in deps])
1056 else:
1057 deps.update(os_deps)
1058
1059 if 'hooks' in local_scope:
1060 self._deps_hooks.extend(local_scope['hooks'])
1061
1062 # If use_relative_paths is set in the DEPS file, regenerate
1063 # the dictionary using paths relative to the directory containing
1064 # the DEPS file.
1065 if local_scope.get('use_relative_paths'):
1066 rel_deps = {}
1067 for d, url in deps.items():
1068 # normpath is required to allow DEPS to use .. in their
1069 # dependency local path.
1070 rel_deps[os.path.normpath(os.path.join(solution_name, d))] = url
1071 return rel_deps
1072 else:
1073 return deps
1074
1075 def _ParseAllDeps(self, solution_urls, solution_deps_content):
1076 """Parse the complete list of dependencies for the client.
1077
1078 Args:
1079 solution_urls: A dict mapping module names (as relative paths) to URLs
1080 corresponding to the solutions specified by the client. This parameter
1081 is passed as an optimization.
1082 solution_deps_content: A dict mapping module names to the content
1083 of their DEPS files
1084
1085 Returns:
1086 A dict mapping module names (as relative paths) to URLs corresponding
1087 to the entire set of dependencies to checkout for the given client.
1088
1089 Raises:
1090 Error: If a dependency conflicts with another dependency or of a solution.
1091 """
1092 deps = {}
1093 for solution in self.GetVar("solutions"):
1094 custom_vars = solution.get("custom_vars", {})
1095 solution_deps = self._ParseSolutionDeps(
1096 solution["name"],
1097 solution_deps_content[solution["name"]],
1098 custom_vars)
1099
1100 # If a line is in custom_deps, but not in the solution, we want to append
1101 # this line to the solution.
1102 if "custom_deps" in solution:
1103 for d in solution["custom_deps"]:
1104 if d not in solution_deps:
1105 solution_deps[d] = solution["custom_deps"][d]
1106
1107 for d in solution_deps:
1108 if "custom_deps" in solution and d in solution["custom_deps"]:
1109 # Dependency is overriden.
1110 url = solution["custom_deps"][d]
1111 if url is None:
1112 continue
1113 else:
1114 url = solution_deps[d]
1115 # if we have a From reference dependent on another solution, then
1116 # just skip the From reference. When we pull deps for the solution,
1117 # we will take care of this dependency.
1118 #
1119 # If multiple solutions all have the same From reference, then we
1120 # should only add one to our list of dependencies.
1121 if type(url) != str:
1122 if url.module_name in solution_urls:
1123 # Already parsed.
1124 continue
1125 if d in deps and type(deps[d]) != str:
1126 if url.module_name == deps[d].module_name:
1127 continue
1128 else:
1129 parsed_url = urlparse.urlparse(url)
1130 scheme = parsed_url[0]
1131 if not scheme:
1132 # A relative url. Fetch the real base.
1133 path = parsed_url[2]
1134 if path[0] != "/":
1135 raise Error(
1136 "relative DEPS entry \"%s\" must begin with a slash" % d)
1137 # Create a scm just to query the full url.
1138 scm = self._options.scm_wrapper(solution["url"], self._root_dir,
1139 None)
1140 url = scm.FullUrlForRelativeUrl(url)
1141 if d in deps and deps[d] != url:
1142 raise Error(
1143 "Solutions have conflicting versions of dependency \"%s\"" % d)
1144 if d in solution_urls and solution_urls[d] != url:
1145 raise Error(
1146 "Dependency \"%s\" conflicts with specified solution" % d)
1147 # Grab the dependency.
1148 deps[d] = url
1149 return deps
1150
1151 def _RunHookAction(self, hook_dict):
1152 """Runs the action from a single hook.
1153 """
1154 command = hook_dict['action'][:]
1155 if command[0] == 'python':
1156 # If the hook specified "python" as the first item, the action is a
1157 # Python script. Run it by starting a new copy of the same
1158 # interpreter.
1159 command[0] = sys.executable
1160
1161 # Use a discrete exit status code of 2 to indicate that a hook action
1162 # failed. Users of this script may wish to treat hook action failures
1163 # differently from VC failures.
1164 SubprocessCall(command, self._root_dir, self._options.stdout,
1165 fail_status=2)
1166
1167 def _RunHooks(self, command, file_list, is_using_git):
1168 """Evaluates all hooks, running actions as needed.
1169 """
1170 # Hooks only run for these command types.
1171 if not command in ('update', 'revert', 'runhooks'):
1172 return
1173
1174 # Get any hooks from the .gclient file.
1175 hooks = self.GetVar("hooks", [])
1176 # Add any hooks found in DEPS files.
1177 hooks.extend(self._deps_hooks)
1178
1179 # If "--force" was specified, run all hooks regardless of what files have
1180 # changed. If the user is using git, then we don't know what files have
1181 # changed so we always run all hooks.
1182 if self._options.force or is_using_git:
1183 for hook_dict in hooks:
1184 self._RunHookAction(hook_dict)
1185 return
1186
1187 # Run hooks on the basis of whether the files from the gclient operation
1188 # match each hook's pattern.
1189 for hook_dict in hooks:
1190 pattern = re.compile(hook_dict['pattern'])
1191 for file in file_list:
1192 if not pattern.search(file):
1193 continue
1194
1195 self._RunHookAction(hook_dict)
1196
1197 # The hook's action only runs once. Don't bother looking for any
1198 # more matches.
1199 break
1200
1201 def RunOnDeps(self, command, args):
1202 """Runs a command on each dependency in a client and its dependencies.
1203
1204 The module's dependencies are specified in its top-level DEPS files.
1205
1206 Args:
1207 command: The command to use (e.g., 'status' or 'diff')
1208 args: list of str - extra arguments to add to the command line.
1209
1210 Raises:
1211 Error: If the client has conflicting entries.
1212 """
1213 if not command in self.supported_commands:
1214 raise Error("'%s' is an unsupported command" % command)
1215
1216 # Check for revision overrides.
1217 revision_overrides = {}
1218 for revision in self._options.revisions:
1219 if revision.find("@") == -1:
1220 raise Error(
1221 "Specify the full dependency when specifying a revision number.")
1222 revision_elem = revision.split("@")
1223 # Disallow conflicting revs
1224 if revision_overrides.has_key(revision_elem[0]) and \
1225 revision_overrides[revision_elem[0]] != revision_elem[1]:
1226 raise Error(
1227 "Conflicting revision numbers specified.")
1228 revision_overrides[revision_elem[0]] = revision_elem[1]
1229
1230 solutions = self.GetVar("solutions")
1231 if not solutions:
1232 raise Error("No solution specified")
1233
1234 # When running runhooks --force, there's no need to consult the SCM.
1235 # All known hooks are expected to run unconditionally regardless of working
1236 # copy state, so skip the SCM status check.
1237 run_scm = not (command == 'runhooks' and self._options.force)
1238
1239 entries = {}
1240 entries_deps_content = {}
1241 file_list = []
1242 # Run on the base solutions first.
1243 for solution in solutions:
1244 name = solution["name"]
1245 if name in entries:
1246 raise Error("solution %s specified more than once" % name)
1247 url = solution["url"]
1248 entries[name] = url
1249 if run_scm:
1250 self._options.revision = revision_overrides.get(name)
1251 scm = self._options.scm_wrapper(url, self._root_dir, name)
1252 scm.RunCommand(command, self._options, args, file_list)
1253 self._options.revision = None
1254 try:
1255 deps_content = FileRead(os.path.join(self._root_dir, name,
1256 self._options.deps_file))
1257 except IOError, e:
1258 if e.errno != errno.ENOENT:
1259 raise
1260 deps_content = ""
1261 entries_deps_content[name] = deps_content
1262
1263 # Process the dependencies next (sort alphanumerically to ensure that
1264 # containing directories get populated first and for readability)
1265 deps = self._ParseAllDeps(entries, entries_deps_content)
1266 deps_to_process = deps.keys()
1267 deps_to_process.sort()
1268
1269 # First pass for direct dependencies.
1270 for d in deps_to_process:
1271 if type(deps[d]) == str:
1272 url = deps[d]
1273 entries[d] = url
1274 if run_scm:
1275 self._options.revision = revision_overrides.get(d)
1276 scm = self._options.scm_wrapper(url, self._root_dir, d)
1277 scm.RunCommand(command, self._options, args, file_list)
1278 self._options.revision = None
1279
1280 # Second pass for inherited deps (via the From keyword)
1281 for d in deps_to_process:
1282 if type(deps[d]) != str:
1283 sub_deps = self._ParseSolutionDeps(
1284 deps[d].module_name,
1285 FileRead(os.path.join(self._root_dir,
1286 deps[d].module_name,
1287 self._options.deps_file)),
1288 {})
1289 url = sub_deps[d]
1290 entries[d] = url
1291 if run_scm:
1292 self._options.revision = revision_overrides.get(d)
1293 scm = self._options.scm_wrapper(url, self._root_dir, d)
1294 scm.RunCommand(command, self._options, args, file_list)
1295 self._options.revision = None
1296
1297 is_using_git = IsUsingGit(self._root_dir, entries.keys())
1298 self._RunHooks(command, file_list, is_using_git)
1299
1300 if command == 'update':
1301 # notify the user if there is an orphaned entry in their working copy.
1302 # TODO(darin): we should delete this directory manually if it doesn't
1303 # have any changes in it.
1304 prev_entries = self._ReadEntries()
1305 for entry in prev_entries:
1306 e_dir = os.path.join(self._root_dir, entry)
1307 if entry not in entries and self._options.path_exists(e_dir):
1308 if CaptureSVNStatus(self._options, e_dir):
1309 # There are modified files in this entry
1310 entries[entry] = None # Keep warning until removed.
1311 print >> self._options.stdout, (
1312 "\nWARNING: \"%s\" is no longer part of this client. "
1313 "It is recommended that you manually remove it.\n") % entry
1314 else:
1315 # Delete the entry
1316 print >> self._options.stdout, ("\n________ deleting \'%s\' " +
1317 "in \'%s\'") % (entry, self._root_dir)
1318 RemoveDirectory(e_dir)
1319 # record the current list of entries for next time
1320 self._SaveEntries(entries)
1321
1322 def PrintRevInfo(self):
1323 """Output revision info mapping for the client and its dependencies. This
1324 allows the capture of a overall "revision" for the source tree that can
1325 be used to reproduce the same tree in the future. The actual output
1326 contains enough information (source paths, svn server urls and revisions)
1327 that it can be used either to generate external svn commands (without
1328 gclient) or as input to gclient's --rev option (with some massaging of
1329 the data).
1330
1331 NOTE: Unlike RunOnDeps this does not require a local checkout and is run
1332 on the Pulse master. It MUST NOT execute hooks.
1333
1334 Raises:
1335 Error: If the client has conflicting entries.
1336 """
1337 # Check for revision overrides.
1338 revision_overrides = {}
1339 for revision in self._options.revisions:
1340 if revision.find("@") < 0:
1341 raise Error(
1342 "Specify the full dependency when specifying a revision number.")
1343 revision_elem = revision.split("@")
1344 # Disallow conflicting revs
1345 if revision_overrides.has_key(revision_elem[0]) and \
1346 revision_overrides[revision_elem[0]] != revision_elem[1]:
1347 raise Error(
1348 "Conflicting revision numbers specified.")
1349 revision_overrides[revision_elem[0]] = revision_elem[1]
1350
1351 solutions = self.GetVar("solutions")
1352 if not solutions:
1353 raise Error("No solution specified")
1354
1355 entries = {}
1356 entries_deps_content = {}
1357
1358 # Inner helper to generate base url and rev tuple (including honoring
1359 # |revision_overrides|)
1360 def GetURLAndRev(name, original_url):
1361 if original_url.find("@") < 0:
1362 if revision_overrides.has_key(name):
1363 return (original_url, int(revision_overrides[name]))
1364 else:
1365 # TODO(aharper): SVN/SCMWrapper cleanup (non-local commandset)
1366 return (original_url, CaptureSVNHeadRevision(self._options,
1367 original_url))
1368 else:
1369 url_components = original_url.split("@")
1370 if revision_overrides.has_key(name):
1371 return (url_components[0], int(revision_overrides[name]))
1372 else:
1373 return (url_components[0], int(url_components[1]))
1374
1375 # Run on the base solutions first.
1376 for solution in solutions:
1377 name = solution["name"]
1378 if name in entries:
1379 raise Error("solution %s specified more than once" % name)
1380 (url, rev) = GetURLAndRev(name, solution["url"])
1381 entries[name] = "%s@%d" % (url, rev)
1382 # TODO(aharper): SVN/SCMWrapper cleanup (non-local commandset)
1383 entries_deps_content[name] = CaptureSVN(
1384 self._options,
1385 ["cat",
1386 "%s/%s@%d" % (url,
1387 self._options.deps_file,
1388 rev)],
1389 os.getcwd())
1390
1391 # Process the dependencies next (sort alphanumerically to ensure that
1392 # containing directories get populated first and for readability)
1393 deps = self._ParseAllDeps(entries, entries_deps_content)
1394 deps_to_process = deps.keys()
1395 deps_to_process.sort()
1396
1397 # First pass for direct dependencies.
1398 for d in deps_to_process:
1399 if type(deps[d]) == str:
1400 (url, rev) = GetURLAndRev(d, deps[d])
1401 entries[d] = "%s@%d" % (url, rev)
1402
1403 # Second pass for inherited deps (via the From keyword)
1404 for d in deps_to_process:
1405 if type(deps[d]) != str:
1406 deps_parent_url = entries[deps[d].module_name]
1407 if deps_parent_url.find("@") < 0:
1408 raise Error("From %s missing revisioned url" % deps[d].module_name)
1409 deps_parent_url_components = deps_parent_url.split("@")
1410 # TODO(aharper): SVN/SCMWrapper cleanup (non-local commandset)
1411 deps_parent_content = CaptureSVN(
1412 self._options,
1413 ["cat",
1414 "%s/%s@%s" % (deps_parent_url_components[0],
1415 self._options.deps_file,
1416 deps_parent_url_components[1])],
1417 os.getcwd())
1418 sub_deps = self._ParseSolutionDeps(
1419 deps[d].module_name,
1420 FileRead(os.path.join(self._root_dir,
1421 deps[d].module_name,
1422 self._options.deps_file)),
1423 {})
1424 (url, rev) = GetURLAndRev(d, sub_deps[d])
1425 entries[d] = "%s@%d" % (url, rev)
1426
1427 print ";".join(["%s,%s" % (x, entries[x]) for x in sorted(entries.keys())])
1428
1429
1430## gclient commands.
1431
1432
1433def DoCleanup(options, args):
1434 """Handle the cleanup subcommand.
1435
1436 Raises:
1437 Error: if client isn't configured properly.
1438 """
1439 client = options.gclient.LoadCurrentConfig(options)
1440 if not client:
1441 raise Error("client not configured; see 'gclient config'")
1442 if options.verbose:
1443 # Print out the .gclient file. This is longer than if we just printed the
1444 # client dict, but more legible, and it might contain helpful comments.
1445 print >>options.stdout, client.ConfigContent()
1446 options.verbose = True
1447 return client.RunOnDeps('cleanup', args)
1448
1449
1450def DoConfig(options, args):
1451 """Handle the config subcommand.
1452
1453 Args:
1454 options: If options.spec set, a string providing contents of config file.
1455 args: The command line args. If spec is not set,
1456 then args[0] is a string URL to get for config file.
1457
1458 Raises:
1459 Error: on usage error
1460 """
1461 if len(args) < 1 and not options.spec:
1462 raise Error("required argument missing; see 'gclient help config'")
1463 if options.path_exists(options.config_filename):
1464 raise Error("%s file already exists in the current directory" %
1465 options.config_filename)
1466 client = options.gclient('.', options)
1467 if options.spec:
1468 client.SetConfig(options.spec)
1469 else:
1470 # TODO(darin): it would be nice to be able to specify an alternate relpath
1471 # for the given URL.
1472 base_url = args[0]
1473 name = args[0].split("/")[-1]
1474 safesync_url = ""
1475 if len(args) > 1:
1476 safesync_url = args[1]
1477 client.SetDefaultConfig(name, base_url, safesync_url)
1478 client.SaveConfig()
1479
1480
1481def DoHelp(options, args):
1482 """Handle the help subcommand giving help for another subcommand.
1483
1484 Raises:
1485 Error: if the command is unknown.
1486 """
1487 if len(args) == 1 and args[0] in COMMAND_USAGE_TEXT:
1488 print >>options.stdout, COMMAND_USAGE_TEXT[args[0]]
1489 else:
1490 raise Error("unknown subcommand '%s'; see 'gclient help'" % args[0])
1491
1492
1493def DoStatus(options, args):
1494 """Handle the status subcommand.
1495
1496 Raises:
1497 Error: if client isn't configured properly.
1498 """
1499 client = options.gclient.LoadCurrentConfig(options)
1500 if not client:
1501 raise Error("client not configured; see 'gclient config'")
1502 if options.verbose:
1503 # Print out the .gclient file. This is longer than if we just printed the
1504 # client dict, but more legible, and it might contain helpful comments.
1505 print >>options.stdout, client.ConfigContent()
1506 options.verbose = True
1507 return client.RunOnDeps('status', args)
1508
1509
1510def DoUpdate(options, args):
1511 """Handle the update and sync subcommands.
1512
1513 Raises:
1514 Error: if client isn't configured properly.
1515 """
1516 client = options.gclient.LoadCurrentConfig(options)
1517
1518 if not client:
1519 raise Error("client not configured; see 'gclient config'")
1520
1521 if not options.head:
1522 solutions = client.GetVar('solutions')
1523 if solutions:
1524 for s in solutions:
1525 if s.get('safesync_url', ''):
1526 # rip through revisions and make sure we're not over-riding
1527 # something that was explicitly passed
1528 has_key = False
1529 for r in options.revisions:
1530 if r.split('@')[0] == s['name']:
1531 has_key = True
1532 break
1533
1534 if not has_key:
1535 handle = urllib.urlopen(s['safesync_url'])
1536 rev = handle.read().strip()
1537 handle.close()
1538 if len(rev):
1539 options.revisions.append(s['name']+'@'+rev)
1540
1541 if options.verbose:
1542 # Print out the .gclient file. This is longer than if we just printed the
1543 # client dict, but more legible, and it might contain helpful comments.
1544 print >>options.stdout, client.ConfigContent()
1545 return client.RunOnDeps('update', args)
1546
1547
1548def DoDiff(options, args):
1549 """Handle the diff subcommand.
1550
1551 Raises:
1552 Error: if client isn't configured properly.
1553 """
1554 client = options.gclient.LoadCurrentConfig(options)
1555 if not client:
1556 raise Error("client not configured; see 'gclient config'")
1557 if options.verbose:
1558 # Print out the .gclient file. This is longer than if we just printed the
1559 # client dict, but more legible, and it might contain helpful comments.
1560 print >>options.stdout, client.ConfigContent()
1561 options.verbose = True
1562 return client.RunOnDeps('diff', args)
1563
1564
1565def DoRevert(options, args):
1566 """Handle the revert subcommand.
1567
1568 Raises:
1569 Error: if client isn't configured properly.
1570 """
1571 client = options.gclient.LoadCurrentConfig(options)
1572 if not client:
1573 raise Error("client not configured; see 'gclient config'")
1574 return client.RunOnDeps('revert', args)
1575
1576
1577def DoRunHooks(options, args):
1578 """Handle the runhooks subcommand.
1579
1580 Raises:
1581 Error: if client isn't configured properly.
1582 """
1583 client = options.gclient.LoadCurrentConfig(options)
1584 if not client:
1585 raise Error("client not configured; see 'gclient config'")
1586 if options.verbose:
1587 # Print out the .gclient file. This is longer than if we just printed the
1588 # client dict, but more legible, and it might contain helpful comments.
1589 print >>options.stdout, client.ConfigContent()
1590 return client.RunOnDeps('runhooks', args)
1591
1592
1593def DoRevInfo(options, args):
1594 """Handle the revinfo subcommand.
1595
1596 Raises:
1597 Error: if client isn't configured properly.
1598 """
1599 client = options.gclient.LoadCurrentConfig(options)
1600 if not client:
1601 raise Error("client not configured; see 'gclient config'")
1602 client.PrintRevInfo()
1603
1604
1605gclient_command_map = {
1606 "cleanup": DoCleanup,
1607 "config": DoConfig,
1608 "diff": DoDiff,
1609 "help": DoHelp,
1610 "status": DoStatus,
1611 "sync": DoUpdate,
1612 "update": DoUpdate,
1613 "revert": DoRevert,
1614 "runhooks": DoRunHooks,
1615 "revinfo" : DoRevInfo,
1616}
1617
1618
1619def DispatchCommand(command, options, args, command_map=None):
1620 """Dispatches the appropriate subcommand based on command line arguments."""
1621 if command_map is None:
1622 command_map = gclient_command_map
1623
1624 if command in command_map:
1625 return command_map[command](options, args)
1626 else:
1627 raise Error("unknown subcommand '%s'; see 'gclient help'" % command)
1628
1629
1630def Main(argv):
1631 """Parse command line arguments and dispatch command."""
1632
1633 option_parser = optparse.OptionParser(usage=DEFAULT_USAGE_TEXT,
1634 version=__version__)
1635 option_parser.disable_interspersed_args()
1636 option_parser.add_option("", "--force", action="store_true", default=False,
1637 help=("(update/sync only) force update even "
1638 "for modules which haven't changed"))
1639 option_parser.add_option("", "--revision", action="append", dest="revisions",
1640 metavar="REV", default=[],
1641 help=("(update/sync only) sync to a specific "
1642 "revision, can be used multiple times for "
1643 "each solution, e.g. --revision=src@123, "
1644 "--revision=internal@32"))
1645 option_parser.add_option("", "--deps", default=None, dest="deps_os",
1646 metavar="OS_LIST",
1647 help=("(update/sync only) sync deps for the "
1648 "specified (comma-separated) platform(s); "
1649 "'all' will sync all platforms"))
1650 option_parser.add_option("", "--spec", default=None,
1651 help=("(config only) create a gclient file "
1652 "containing the provided string"))
1653 option_parser.add_option("", "--verbose", action="store_true", default=False,
1654 help="produce additional output for diagnostics")
1655 option_parser.add_option("", "--manually_grab_svn_rev", action="store_true",
1656 default=False,
1657 help="Skip svn up whenever possible by requesting "
1658 "actual HEAD revision from the repository")
1659 option_parser.add_option("", "--head", action="store_true", default=False,
1660 help=("skips any safesync_urls specified in "
1661 "configured solutions"))
1662
1663 if len(argv) < 2:
1664 # Users don't need to be told to use the 'help' command.
1665 option_parser.print_help()
1666 return 1
1667 # Add manual support for --version as first argument.
1668 if argv[1] == '--version':
1669 option_parser.print_version()
1670 return 0
1671
1672 # Add manual support for --help as first argument.
1673 if argv[1] == '--help':
1674 argv[1] = 'help'
1675
1676 command = argv[1]
1677 options, args = option_parser.parse_args(argv[2:])
1678
1679 if len(argv) < 3 and command == "help":
1680 option_parser.print_help()
1681 return 0
1682
1683 # Files used for configuration and state saving.
1684 options.config_filename = os.environ.get("GCLIENT_FILE", ".gclient")
1685 options.entries_filename = ".gclient_entries"
1686 options.deps_file = "DEPS"
1687
1688 # These are overridded when testing. They are not externally visible.
1689 options.stdout = sys.stdout
1690 options.path_exists = os.path.exists
1691 options.gclient = GClient
1692 options.scm_wrapper = SCMWrapper
1693 options.platform = sys.platform
1694 return DispatchCommand(command, options, args)
1695
1696
1697if "__main__" == __name__:
1698 try:
1699 result = Main(sys.argv)
1700 except Error, e:
1701 print "Error: %s" % str(e)
1702 result = 1
1703 sys.exit(result)
1704
1705# vim: ts=2:sw=2:tw=80:et: