blob: 6706344ff54cfec44eed75b4ff93a75d3a23eb76 [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
evan@chromium.org67820ef2009-07-27 17:23:00 +000040 working copy as a result of a "sync"/"update" or "revert" operation. This
41 could be prevented by using --nohooks (hooks run by default). Hooks can also
maruel@chromium.org5df6a462009-08-28 18:52:26 +000042 be forced to run with the "runhooks" operation. If "sync" is run with
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000043 --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
phajdan.jr@chromium.org71b40682009-07-31 23:40:09 +000057 to run the command. If the list contains string "$matching_files"
58 it will be removed from the list and the list will be extended
59 by the list of matching files.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000060
61 Example:
62 hooks = [
63 { "pattern": "\\.(gif|jpe?g|pr0n|png)$",
64 "action": ["python", "image_indexer.py", "--all"]},
65 ]
66"""
67
68__author__ = "darinf@gmail.com (Darin Fisher)"
maruel@chromium.org5df6a462009-08-28 18:52:26 +000069__version__ = "0.3.3"
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000070
71import errno
72import optparse
73import os
74import re
75import stat
76import subprocess
77import sys
78import time
79import urlparse
80import xml.dom.minidom
81import urllib
82
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000083
84SVN_COMMAND = "svn"
85
86
87# default help text
88DEFAULT_USAGE_TEXT = (
89"""usage: %prog <subcommand> [options] [--] [svn options/args...]
90a wrapper for managing a set of client modules in svn.
91Version """ + __version__ + """
92
93subcommands:
94 cleanup
95 config
96 diff
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +000097 export
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000098 revert
99 status
100 sync
101 update
102 runhooks
103 revinfo
104
105Options and extra arguments can be passed to invoked svn commands by
106appending them to the command line. Note that if the first such
107appended option starts with a dash (-) then the options must be
108preceded by -- to distinguish them from gclient options.
109
110For additional help on a subcommand or examples of usage, try
111 %prog help <subcommand>
112 %prog help files
113""")
114
115GENERIC_UPDATE_USAGE_TEXT = (
116 """Perform a checkout/update of the modules specified by the gclient
117configuration; see 'help config'. Unless --revision is specified,
118then the latest revision of the root solutions is checked out, with
119dependent submodule versions updated according to DEPS files.
120If --revision is specified, then the given revision is used in place
121of the latest, either for a single solution or for all solutions.
122Unless the --force option is provided, solutions and modules whose
123local revision matches the one to update (i.e., they have not changed
evan@chromium.org67820ef2009-07-27 17:23:00 +0000124in the repository) are *not* modified. Unless --nohooks is provided,
125the hooks are run.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000126This a synonym for 'gclient %(alias)s'
127
128usage: gclient %(cmd)s [options] [--] [svn update options/args]
129
130Valid options:
131 --force : force update even for unchanged modules
evan@chromium.org67820ef2009-07-27 17:23:00 +0000132 --nohooks : don't run the hooks after the update is complete
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000133 --revision REV : update/checkout all solutions with specified revision
134 --revision SOLUTION@REV : update given solution to specified revision
135 --deps PLATFORM(S) : sync deps for the given platform(s), or 'all'
136 --verbose : output additional diagnostics
maruel@chromium.orgb8b6b872009-06-30 18:50:56 +0000137 --head : update to latest revision, instead of last good revision
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000138
139Examples:
140 gclient %(cmd)s
141 update files from SVN according to current configuration,
142 *for modules which have changed since last update or sync*
143 gclient %(cmd)s --force
144 update files from SVN according to current configuration, for
145 all modules (useful for recovering files deleted from local copy)
146""")
147
148COMMAND_USAGE_TEXT = {
149 "cleanup":
150 """Clean up all working copies, using 'svn cleanup' for each module.
151Additional options and args may be passed to 'svn cleanup'.
152
153usage: cleanup [options] [--] [svn cleanup args/options]
154
155Valid options:
156 --verbose : output additional diagnostics
157""",
158 "config": """Create a .gclient file in the current directory; this
159specifies the configuration for further commands. After update/sync,
160top-level DEPS files in each module are read to determine dependent
161modules to operate on as well. If optional [url] parameter is
162provided, then configuration is read from a specified Subversion server
163URL. Otherwise, a --spec option must be provided.
164
165usage: config [option | url] [safesync url]
166
167Valid options:
168 --spec=GCLIENT_SPEC : contents of .gclient are read from string parameter.
169 *Note that due to Cygwin/Python brokenness, it
170 probably can't contain any newlines.*
171
172Examples:
173 gclient config https://gclient.googlecode.com/svn/trunk/gclient
174 configure a new client to check out gclient.py tool sources
175 gclient config --spec='solutions=[{"name":"gclient","""
176 '"url":"https://gclient.googlecode.com/svn/trunk/gclient",'
177 '"custom_deps":{}}]',
178 "diff": """Display the differences between two revisions of modules.
179(Does 'svn diff' for each checked out module and dependences.)
180Additional args and options to 'svn diff' can be passed after
181gclient options.
182
183usage: diff [options] [--] [svn args/options]
184
185Valid options:
186 --verbose : output additional diagnostics
187
188Examples:
189 gclient diff
190 simple 'svn diff' for configured client and dependences
191 gclient diff -- -x -b
192 use 'svn diff -x -b' to suppress whitespace-only differences
193 gclient diff -- -r HEAD -x -b
194 diff versus the latest version of each module
195""",
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +0000196 "export":
197 """Wrapper for svn export for all managed directories
198""",
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000199 "revert":
200 """Revert every file in every managed directory in the client view.
201
202usage: revert
203""",
204 "status":
205 """Show the status of client and dependent modules, using 'svn diff'
206for each module. Additional options and args may be passed to 'svn diff'.
207
208usage: status [options] [--] [svn diff args/options]
209
210Valid options:
211 --verbose : output additional diagnostics
evan@chromium.org67820ef2009-07-27 17:23:00 +0000212 --nohooks : don't run the hooks after the update is complete
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000213""",
214 "sync": GENERIC_UPDATE_USAGE_TEXT % {"cmd": "sync", "alias": "update"},
215 "update": GENERIC_UPDATE_USAGE_TEXT % {"cmd": "update", "alias": "sync"},
216 "help": """Describe the usage of this program or its subcommands.
217
218usage: help [options] [subcommand]
219
220Valid options:
221 --verbose : output additional diagnostics
222""",
223 "runhooks":
224 """Runs hooks for files that have been modified in the local working copy,
maruel@chromium.org5df6a462009-08-28 18:52:26 +0000225according to 'svn status'. Implies --force.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000226
227usage: runhooks [options]
228
229Valid options:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000230 --verbose : output additional diagnostics
231""",
232 "revinfo":
233 """Outputs source path, server URL and revision information for every
234dependency in all solutions (no local checkout required).
235
236usage: revinfo [options]
237""",
238}
239
240# parameterized by (solution_name, solution_url, safesync_url)
241DEFAULT_CLIENT_FILE_TEXT = (
242 """
243# An element of this array (a \"solution\") describes a repository directory
244# that will be checked out into your working copy. Each solution may
245# optionally define additional dependencies (via its DEPS file) to be
246# checked out alongside the solution's directory. A solution may also
247# specify custom dependencies (via the \"custom_deps\" property) that
248# override or augment the dependencies specified by the DEPS file.
249# If a \"safesync_url\" is specified, it is assumed to reference the location of
250# a text file which contains nothing but the last known good SCM revision to
251# sync against. It is fetched if specified and used unless --head is passed
252solutions = [
253 { \"name\" : \"%s\",
254 \"url\" : \"%s\",
255 \"custom_deps\" : {
256 # To use the trunk of a component instead of what's in DEPS:
257 #\"component\": \"https://svnserver/component/trunk/\",
258 # To exclude a component from your working copy:
259 #\"data/really_large_component\": None,
260 },
261 \"safesync_url\": \"%s\"
262 }
263]
264""")
265
266
267## Generic utils
268
maruel@chromium.orge105d8d2009-04-30 17:58:25 +0000269def ParseXML(output):
270 try:
271 return xml.dom.minidom.parseString(output)
272 except xml.parsers.expat.ExpatError:
273 return None
274
275
maruel@chromium.org483b0082009-05-07 02:57:14 +0000276def GetNamedNodeText(node, node_name):
277 child_nodes = node.getElementsByTagName(node_name)
278 if not child_nodes:
279 return None
280 assert len(child_nodes) == 1 and child_nodes[0].childNodes.length == 1
281 return child_nodes[0].firstChild.nodeValue
282
283
284def GetNodeNamedAttributeText(node, node_name, attribute_name):
285 child_nodes = node.getElementsByTagName(node_name)
286 if not child_nodes:
287 return None
288 assert len(child_nodes) == 1
289 return child_nodes[0].getAttribute(attribute_name)
290
291
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000292class Error(Exception):
293 """gclient exception class."""
294 pass
295
296class PrintableObject(object):
297 def __str__(self):
298 output = ''
299 for i in dir(self):
300 if i.startswith('__'):
301 continue
302 output += '%s = %s\n' % (i, str(getattr(self, i, '')))
303 return output
304
305
306def FileRead(filename):
307 content = None
308 f = open(filename, "rU")
309 try:
310 content = f.read()
311 finally:
312 f.close()
313 return content
314
315
316def FileWrite(filename, content):
317 f = open(filename, "w")
318 try:
319 f.write(content)
320 finally:
321 f.close()
322
323
324def RemoveDirectory(*path):
325 """Recursively removes a directory, even if it's marked read-only.
326
327 Remove the directory located at *path, if it exists.
328
329 shutil.rmtree() doesn't work on Windows if any of the files or directories
330 are read-only, which svn repositories and some .svn files are. We need to
331 be able to force the files to be writable (i.e., deletable) as we traverse
332 the tree.
333
334 Even with all this, Windows still sometimes fails to delete a file, citing
335 a permission error (maybe something to do with antivirus scans or disk
336 indexing). The best suggestion any of the user forums had was to wait a
337 bit and try again, so we do that too. It's hand-waving, but sometimes it
338 works. :/
339
340 On POSIX systems, things are a little bit simpler. The modes of the files
341 to be deleted doesn't matter, only the modes of the directories containing
342 them are significant. As the directory tree is traversed, each directory
343 has its mode set appropriately before descending into it. This should
344 result in the entire tree being removed, with the possible exception of
345 *path itself, because nothing attempts to change the mode of its parent.
346 Doing so would be hazardous, as it's not a directory slated for removal.
347 In the ordinary case, this is not a problem: for our purposes, the user
348 will never lack write permission on *path's parent.
349 """
350 file_path = os.path.join(*path)
351 if not os.path.exists(file_path):
352 return
353
354 if os.path.islink(file_path) or not os.path.isdir(file_path):
355 raise Error("RemoveDirectory asked to remove non-directory %s" % file_path)
356
357 has_win32api = False
358 if sys.platform == 'win32':
359 has_win32api = True
360 # Some people don't have the APIs installed. In that case we'll do without.
361 try:
362 win32api = __import__('win32api')
363 win32con = __import__('win32con')
364 except ImportError:
365 has_win32api = False
366 else:
367 # On POSIX systems, we need the x-bit set on the directory to access it,
368 # the r-bit to see its contents, and the w-bit to remove files from it.
369 # The actual modes of the files within the directory is irrelevant.
370 os.chmod(file_path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
371 for fn in os.listdir(file_path):
372 fullpath = os.path.join(file_path, fn)
373
374 # If fullpath is a symbolic link that points to a directory, isdir will
375 # be True, but we don't want to descend into that as a directory, we just
376 # want to remove the link. Check islink and treat links as ordinary files
377 # would be treated regardless of what they reference.
378 if os.path.islink(fullpath) or not os.path.isdir(fullpath):
379 if sys.platform == 'win32':
380 os.chmod(fullpath, stat.S_IWRITE)
381 if has_win32api:
382 win32api.SetFileAttributes(fullpath, win32con.FILE_ATTRIBUTE_NORMAL)
383 try:
384 os.remove(fullpath)
385 except OSError, e:
386 if e.errno != errno.EACCES or sys.platform != 'win32':
387 raise
388 print 'Failed to delete %s: trying again' % fullpath
389 time.sleep(0.1)
390 os.remove(fullpath)
391 else:
392 RemoveDirectory(fullpath)
393
394 if sys.platform == 'win32':
395 os.chmod(file_path, stat.S_IWRITE)
396 if has_win32api:
397 win32api.SetFileAttributes(file_path, win32con.FILE_ATTRIBUTE_NORMAL)
398 try:
399 os.rmdir(file_path)
400 except OSError, e:
401 if e.errno != errno.EACCES or sys.platform != 'win32':
402 raise
403 print 'Failed to remove %s: trying again' % file_path
404 time.sleep(0.1)
405 os.rmdir(file_path)
406
407
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000408def SubprocessCall(command, in_directory, fail_status=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000409 """Runs command, a list, in directory in_directory.
410
411 This function wraps SubprocessCallAndCapture, but does not perform the
412 capturing functions. See that function for a more complete usage
413 description.
414 """
415 # Call subprocess and capture nothing:
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000416 SubprocessCallAndCapture(command, in_directory, fail_status)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000417
418
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000419def SubprocessCallAndCapture(command, in_directory, fail_status=None,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000420 pattern=None, capture_list=None):
421 """Runs command, a list, in directory in_directory.
422
423 A message indicating what is being done, as well as the command's stdout,
424 is printed to out.
425
426 If a pattern is specified, any line in the output matching pattern will have
427 its first match group appended to capture_list.
428
429 If the command fails, as indicated by a nonzero exit status, gclient will
430 exit with an exit status of fail_status. If fail_status is None (the
431 default), gclient will raise an Error exception.
432 """
433
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000434 print("\n________ running \'%s\' in \'%s\'"
435 % (' '.join(command), in_directory))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000436
437 # *Sigh*: Windows needs shell=True, or else it won't search %PATH% for the
438 # executable, but shell=True makes subprocess on Linux fail when it's called
439 # with a list because it only tries to execute the first item in the list.
440 kid = subprocess.Popen(command, bufsize=0, cwd=in_directory,
441 shell=(sys.platform == 'win32'), stdout=subprocess.PIPE)
442
443 if pattern:
444 compiled_pattern = re.compile(pattern)
445
446 # Also, we need to forward stdout to prevent weird re-ordering of output.
447 # This has to be done on a per byte basis to make sure it is not buffered:
448 # normally buffering is done for each line, but if svn requests input, no
449 # end-of-line character is output after the prompt and it would not show up.
450 in_byte = kid.stdout.read(1)
451 in_line = ""
452 while in_byte:
453 if in_byte != "\r":
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000454 sys.stdout.write(in_byte)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000455 in_line += in_byte
456 if in_byte == "\n" and pattern:
457 match = compiled_pattern.search(in_line[:-1])
458 if match:
459 capture_list.append(match.group(1))
460 in_line = ""
461 in_byte = kid.stdout.read(1)
462 rv = kid.wait()
463
464 if rv:
465 msg = "failed to run command: %s" % " ".join(command)
466
467 if fail_status != None:
468 print >>sys.stderr, msg
469 sys.exit(fail_status)
470
471 raise Error(msg)
472
473
474def IsUsingGit(root, paths):
475 """Returns True if we're using git to manage any of our checkouts.
476 |entries| is a list of paths to check."""
477 for path in paths:
478 if os.path.exists(os.path.join(root, path, '.git')):
479 return True
480 return False
481
482# -----------------------------------------------------------------------------
483# SVN utils:
484
485
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000486def RunSVN(args, in_directory):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000487 """Runs svn, sending output to stdout.
488
489 Args:
490 args: A sequence of command line parameters to be passed to svn.
491 in_directory: The directory where svn is to be run.
492
493 Raises:
494 Error: An error occurred while running the svn command.
495 """
496 c = [SVN_COMMAND]
497 c.extend(args)
498
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000499 SubprocessCall(c, in_directory)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000500
501
maruel@chromium.org5c3a2ff2009-05-12 19:28:55 +0000502def CaptureSVN(args, in_directory=None, print_error=True):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000503 """Runs svn, capturing output sent to stdout as a string.
504
505 Args:
506 args: A sequence of command line parameters to be passed to svn.
507 in_directory: The directory where svn is to be run.
508
509 Returns:
510 The output sent to stdout as a string.
511 """
512 c = [SVN_COMMAND]
513 c.extend(args)
514
515 # *Sigh*: Windows needs shell=True, or else it won't search %PATH% for
516 # the svn.exe executable, but shell=True makes subprocess on Linux fail
517 # when it's called with a list because it only tries to execute the
518 # first string ("svn").
maruel@chromium.org5c3a2ff2009-05-12 19:28:55 +0000519 stderr = None
maruel@chromium.org672343d2009-05-20 20:03:25 +0000520 if not print_error:
maruel@chromium.org5c3a2ff2009-05-12 19:28:55 +0000521 stderr = subprocess.PIPE
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000522 return subprocess.Popen(c,
523 cwd=in_directory,
524 shell=(sys.platform == 'win32'),
maruel@chromium.org5c3a2ff2009-05-12 19:28:55 +0000525 stdout=subprocess.PIPE,
526 stderr=stderr).communicate()[0]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000527
528
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000529def RunSVNAndGetFileList(args, in_directory, file_list):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000530 """Runs svn checkout, update, or status, output to stdout.
531
532 The first item in args must be either "checkout", "update", or "status".
533
534 svn's stdout is parsed to collect a list of files checked out or updated.
535 These files are appended to file_list. svn's stdout is also printed to
536 sys.stdout as in RunSVN.
537
538 Args:
539 args: A sequence of command line parameters to be passed to svn.
540 in_directory: The directory where svn is to be run.
541
542 Raises:
543 Error: An error occurred while running the svn command.
544 """
545 command = [SVN_COMMAND]
546 command.extend(args)
547
548 # svn update and svn checkout use the same pattern: the first three columns
549 # are for file status, property status, and lock status. This is followed
550 # by two spaces, and then the path to the file.
551 update_pattern = '^... (.*)$'
552
553 # The first three columns of svn status are the same as for svn update and
554 # svn checkout. The next three columns indicate addition-with-history,
555 # switch, and remote lock status. This is followed by one space, and then
556 # the path to the file.
557 status_pattern = '^...... (.*)$'
558
559 # args[0] must be a supported command. This will blow up if it's something
560 # else, which is good. Note that the patterns are only effective when
561 # these commands are used in their ordinary forms, the patterns are invalid
562 # for "svn status --show-updates", for example.
563 pattern = {
564 'checkout': update_pattern,
565 'status': status_pattern,
566 'update': update_pattern,
567 }[args[0]]
568
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000569 SubprocessCallAndCapture(command,
570 in_directory,
571 pattern=pattern,
572 capture_list=file_list)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000573
574
maruel@chromium.org5c3a2ff2009-05-12 19:28:55 +0000575def CaptureSVNInfo(relpath, in_directory=None, print_error=True):
maruel@chromium.org2dc8a4d2009-05-11 12:41:20 +0000576 """Returns a dictionary from the svn info output for the given file.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000577
578 Args:
579 relpath: The directory where the working copy resides relative to
580 the directory given by in_directory.
581 in_directory: The directory where svn is to be run.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000582 """
maruel@chromium.org5c3a2ff2009-05-12 19:28:55 +0000583 output = CaptureSVN(["info", "--xml", relpath], in_directory, print_error)
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000584 dom = ParseXML(output)
maruel@chromium.org2dc8a4d2009-05-11 12:41:20 +0000585 result = {}
maruel@chromium.org483b0082009-05-07 02:57:14 +0000586 if dom:
maruel@chromium.org2dc8a4d2009-05-11 12:41:20 +0000587 def C(item, f):
588 if item is not None: return f(item)
maruel@chromium.org483b0082009-05-07 02:57:14 +0000589 # /info/entry/
590 # url
591 # reposityory/(root|uuid)
592 # wc-info/(schedule|depth)
593 # commit/(author|date)
594 # str() the results because they may be returned as Unicode, which
595 # interferes with the higher layers matching up things in the deps
596 # dictionary.
maruel@chromium.org2dc8a4d2009-05-11 12:41:20 +0000597 # TODO(maruel): Fix at higher level instead (!)
598 result['Repository Root'] = C(GetNamedNodeText(dom, 'root'), str)
599 result['URL'] = C(GetNamedNodeText(dom, 'url'), str)
600 result['UUID'] = C(GetNamedNodeText(dom, 'uuid'), str)
601 result['Revision'] = C(GetNodeNamedAttributeText(dom, 'entry', 'revision'),
602 int)
603 result['Node Kind'] = C(GetNodeNamedAttributeText(dom, 'entry', 'kind'),
604 str)
605 result['Schedule'] = C(GetNamedNodeText(dom, 'schedule'), str)
606 result['Path'] = C(GetNodeNamedAttributeText(dom, 'entry', 'path'), str)
607 result['Copied From URL'] = C(GetNamedNodeText(dom, 'copy-from-url'), str)
608 result['Copied From Rev'] = C(GetNamedNodeText(dom, 'copy-from-rev'), str)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000609 return result
610
611
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000612def CaptureSVNHeadRevision(url):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000613 """Get the head revision of a SVN repository.
614
615 Returns:
616 Int head revision
617 """
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000618 info = CaptureSVN(["info", "--xml", url], os.getcwd())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000619 dom = xml.dom.minidom.parseString(info)
620 return int(dom.getElementsByTagName('entry')[0].getAttribute('revision'))
621
622
maruel@chromium.org4810a962009-05-12 21:03:34 +0000623def CaptureSVNStatus(files):
624 """Returns the svn 1.5 svn status emulated output.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000625
maruel@chromium.org4810a962009-05-12 21:03:34 +0000626 @files can be a string (one file) or a list of files.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000627
maruel@chromium.org4810a962009-05-12 21:03:34 +0000628 Returns an array of (status, file) tuples."""
629 command = ["status", "--xml"]
630 if not files:
631 pass
632 elif isinstance(files, basestring):
633 command.append(files)
634 else:
635 command.extend(files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000636
maruel@chromium.org4810a962009-05-12 21:03:34 +0000637 status_letter = {
638 None: ' ',
639 '': ' ',
640 'added': 'A',
641 'conflicted': 'C',
642 'deleted': 'D',
643 'external': 'X',
644 'ignored': 'I',
645 'incomplete': '!',
646 'merged': 'G',
647 'missing': '!',
648 'modified': 'M',
649 'none': ' ',
650 'normal': ' ',
651 'obstructed': '~',
652 'replaced': 'R',
653 'unversioned': '?',
654 }
655 dom = ParseXML(CaptureSVN(command))
maruel@chromium.orge105d8d2009-04-30 17:58:25 +0000656 results = []
657 if dom:
658 # /status/target/entry/(wc-status|commit|author|date)
659 for target in dom.getElementsByTagName('target'):
660 base_path = target.getAttribute('path')
661 for entry in target.getElementsByTagName('entry'):
662 file = entry.getAttribute('path')
663 wc_status = entry.getElementsByTagName('wc-status')
664 assert len(wc_status) == 1
665 # Emulate svn 1.5 status ouput...
666 statuses = [' ' for i in range(7)]
667 # Col 0
668 xml_item_status = wc_status[0].getAttribute('item')
maruel@chromium.org4810a962009-05-12 21:03:34 +0000669 if xml_item_status in status_letter:
670 statuses[0] = status_letter[xml_item_status]
maruel@chromium.orge105d8d2009-04-30 17:58:25 +0000671 else:
672 raise Exception('Unknown item status "%s"; please implement me!' %
673 xml_item_status)
674 # Col 1
675 xml_props_status = wc_status[0].getAttribute('props')
676 if xml_props_status == 'modified':
677 statuses[1] = 'M'
678 elif xml_props_status == 'conflicted':
679 statuses[1] = 'C'
680 elif (not xml_props_status or xml_props_status == 'none' or
681 xml_props_status == 'normal'):
682 pass
683 else:
684 raise Exception('Unknown props status "%s"; please implement me!' %
685 xml_props_status)
maruel@chromium.orgedd27d12009-05-01 17:46:56 +0000686 # Col 2
687 if wc_status[0].getAttribute('wc-locked') == 'true':
688 statuses[2] = 'L'
maruel@chromium.orge105d8d2009-04-30 17:58:25 +0000689 # Col 3
690 if wc_status[0].getAttribute('copied') == 'true':
691 statuses[3] = '+'
maruel@chromium.org4810a962009-05-12 21:03:34 +0000692 item = (''.join(statuses), file)
maruel@chromium.orge105d8d2009-04-30 17:58:25 +0000693 results.append(item)
694 return results
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000695
696
697### SCM abstraction layer
698
699
700class SCMWrapper(object):
701 """Add necessary glue between all the supported SCM.
702
703 This is the abstraction layer to bind to different SCM. Since currently only
704 subversion is supported, a lot of subersionism remains. This can be sorted out
705 once another SCM is supported."""
706 def __init__(self, url=None, root_dir=None, relpath=None,
707 scm_name='svn'):
708 # TODO(maruel): Deduce the SCM from the url.
709 self.scm_name = scm_name
710 self.url = url
711 self._root_dir = root_dir
712 if self._root_dir:
maruel@chromium.orge105d8d2009-04-30 17:58:25 +0000713 self._root_dir = self._root_dir.replace('/', os.sep)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000714 self.relpath = relpath
715 if self.relpath:
maruel@chromium.orge105d8d2009-04-30 17:58:25 +0000716 self.relpath = self.relpath.replace('/', os.sep)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000717
718 def FullUrlForRelativeUrl(self, url):
719 # Find the forth '/' and strip from there. A bit hackish.
720 return '/'.join(self.url.split('/')[:4]) + url
721
722 def RunCommand(self, command, options, args, file_list=None):
723 # file_list will have all files that are modified appended to it.
724
725 if file_list == None:
726 file_list = []
727
728 commands = {
729 'cleanup': self.cleanup,
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +0000730 'export': self.export,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000731 'update': self.update,
732 'revert': self.revert,
733 'status': self.status,
734 'diff': self.diff,
735 'runhooks': self.status,
736 }
737
738 if not command in commands:
739 raise Error('Unknown command %s' % command)
740
741 return commands[command](options, args, file_list)
742
743 def cleanup(self, options, args, file_list):
744 """Cleanup working copy."""
745 command = ['cleanup']
746 command.extend(args)
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000747 RunSVN(command, os.path.join(self._root_dir, self.relpath))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000748
749 def diff(self, options, args, file_list):
750 # NOTE: This function does not currently modify file_list.
751 command = ['diff']
752 command.extend(args)
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000753 RunSVN(command, os.path.join(self._root_dir, self.relpath))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000754
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +0000755 def export(self, options, args, file_list):
756 assert len(args) == 1
757 export_path = os.path.abspath(os.path.join(args[0], self.relpath))
758 try:
759 os.makedirs(export_path)
760 except OSError:
761 pass
762 assert os.path.exists(export_path)
763 command = ['export', '--force', '.']
764 command.append(export_path)
765 RunSVN(command, os.path.join(self._root_dir, self.relpath))
766
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000767 def update(self, options, args, file_list):
768 """Runs SCM to update or transparently checkout the working copy.
769
770 All updated files will be appended to file_list.
771
772 Raises:
773 Error: if can't get URL for relative path.
774 """
775 # Only update if git is not controlling the directory.
maruel@chromium.org8626ff72009-05-13 02:57:02 +0000776 checkout_path = os.path.join(self._root_dir, self.relpath)
maruel@chromium.org0329e672009-05-13 18:41:04 +0000777 git_path = os.path.join(self._root_dir, self.relpath, '.git')
778 if os.path.exists(git_path):
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000779 print("________ found .git directory; skipping %s" % self.relpath)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000780 return
781
782 if args:
783 raise Error("Unsupported argument(s): %s" % ",".join(args))
784
785 url = self.url
786 components = url.split("@")
787 revision = None
788 forced_revision = False
789 if options.revision:
790 # Override the revision number.
791 url = '%s@%s' % (components[0], str(options.revision))
792 revision = int(options.revision)
793 forced_revision = True
794 elif len(components) == 2:
795 revision = int(components[1])
796 forced_revision = True
797
798 rev_str = ""
799 if revision:
800 rev_str = ' at %d' % revision
801
maruel@chromium.org0329e672009-05-13 18:41:04 +0000802 if not os.path.exists(checkout_path):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000803 # We need to checkout.
maruel@chromium.org8626ff72009-05-13 02:57:02 +0000804 command = ['checkout', url, checkout_path]
maruel@chromium.orgedd27d12009-05-01 17:46:56 +0000805 if revision:
806 command.extend(['--revision', str(revision)])
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000807 RunSVNAndGetFileList(command, self._root_dir, file_list)
maruel@chromium.orgedd27d12009-05-01 17:46:56 +0000808 return
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000809
810 # Get the existing scm url and the revision number of the current checkout.
maruel@chromium.org8626ff72009-05-13 02:57:02 +0000811 from_info = CaptureSVNInfo(os.path.join(checkout_path, '.'), '.')
maruel@chromium.org1998c6d2009-05-15 12:38:12 +0000812 if not from_info:
813 raise Error("Can't update/checkout %r if an unversioned directory is "
814 "present. Delete the directory and try again." %
815 checkout_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000816
817 if options.manually_grab_svn_rev:
818 # Retrieve the current HEAD version because svn is slow at null updates.
819 if not revision:
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000820 from_info_live = CaptureSVNInfo(from_info['URL'], '.')
maruel@chromium.org2dc8a4d2009-05-11 12:41:20 +0000821 revision = int(from_info_live['Revision'])
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000822 rev_str = ' at %d' % revision
823
maruel@chromium.org2dc8a4d2009-05-11 12:41:20 +0000824 if from_info['URL'] != components[0]:
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000825 to_info = CaptureSVNInfo(url, '.')
maruel@chromium.org8626ff72009-05-13 02:57:02 +0000826 can_switch = ((from_info['Repository Root'] != to_info['Repository Root'])
827 and (from_info['UUID'] == to_info['UUID']))
828 if can_switch:
829 print("\n_____ relocating %s to a new checkout" % self.relpath)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000830 # We have different roots, so check if we can switch --relocate.
831 # Subversion only permits this if the repository UUIDs match.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000832 # Perform the switch --relocate, then rewrite the from_url
833 # to reflect where we "are now." (This is the same way that
834 # Subversion itself handles the metadata when switch --relocate
835 # is used.) This makes the checks below for whether we
836 # can update to a revision or have to switch to a different
837 # branch work as expected.
838 # TODO(maruel): TEST ME !
maruel@chromium.org2dc8a4d2009-05-11 12:41:20 +0000839 command = ["switch", "--relocate",
840 from_info['Repository Root'],
841 to_info['Repository Root'],
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000842 self.relpath]
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000843 RunSVN(command, self._root_dir)
maruel@chromium.org2dc8a4d2009-05-11 12:41:20 +0000844 from_info['URL'] = from_info['URL'].replace(
845 from_info['Repository Root'],
846 to_info['Repository Root'])
maruel@chromium.org8626ff72009-05-13 02:57:02 +0000847 else:
848 if CaptureSVNStatus(checkout_path):
849 raise Error("Can't switch the checkout to %s; UUID don't match and "
850 "there is local changes in %s. Delete the directory and "
851 "try again." % (url, checkout_path))
852 # Ok delete it.
853 print("\n_____ switching %s to a new checkout" % self.relpath)
854 RemoveDirectory(checkout_path)
855 # We need to checkout.
856 command = ['checkout', url, checkout_path]
857 if revision:
858 command.extend(['--revision', str(revision)])
859 RunSVNAndGetFileList(command, self._root_dir, file_list)
860 return
phajdan.jr@chromium.org71b40682009-07-31 23:40:09 +0000861
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000862
863 # If the provided url has a revision number that matches the revision
864 # number of the existing directory, then we don't need to bother updating.
maruel@chromium.org2dc8a4d2009-05-11 12:41:20 +0000865 if not options.force and from_info['Revision'] == revision:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000866 if options.verbose or not forced_revision:
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000867 print("\n_____ %s%s" % (self.relpath, rev_str))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000868 return
869
maruel@chromium.org8626ff72009-05-13 02:57:02 +0000870 command = ["update", checkout_path]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000871 if revision:
872 command.extend(['--revision', str(revision)])
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000873 RunSVNAndGetFileList(command, self._root_dir, file_list)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000874
875 def revert(self, options, args, file_list):
876 """Reverts local modifications. Subversion specific.
877
878 All reverted files will be appended to file_list, even if Subversion
879 doesn't know about them.
880 """
881 path = os.path.join(self._root_dir, self.relpath)
882 if not os.path.isdir(path):
maruel@chromium.orgedd27d12009-05-01 17:46:56 +0000883 # svn revert won't work if the directory doesn't exist. It needs to
884 # checkout instead.
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000885 print("\n_____ %s is missing, synching instead" % self.relpath)
maruel@chromium.orgedd27d12009-05-01 17:46:56 +0000886 # Don't reuse the args.
887 return self.update(options, [], file_list)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000888
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000889 files = CaptureSVNStatus(path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000890 # Batch the command.
891 files_to_revert = []
892 for file in files:
maruel@chromium.org4810a962009-05-12 21:03:34 +0000893 file_path = os.path.join(path, file[1])
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000894 print(file_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000895 # Unversioned file or unexpected unversioned file.
maruel@chromium.org4810a962009-05-12 21:03:34 +0000896 if file[0][0] in ('?', '~'):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000897 # Remove extraneous file. Also remove unexpected unversioned
898 # directories. svn won't touch them but we want to delete these.
899 file_list.append(file_path)
900 try:
901 os.remove(file_path)
902 except EnvironmentError:
903 RemoveDirectory(file_path)
904
maruel@chromium.org4810a962009-05-12 21:03:34 +0000905 if file[0][0] != '?':
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000906 # For any other status, svn revert will work.
907 file_list.append(file_path)
maruel@chromium.org4810a962009-05-12 21:03:34 +0000908 files_to_revert.append(file[1])
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000909
910 # Revert them all at once.
911 if files_to_revert:
912 accumulated_paths = []
913 accumulated_length = 0
914 command = ['revert']
915 for p in files_to_revert:
916 # Some shell have issues with command lines too long.
917 if accumulated_length and accumulated_length + len(p) > 3072:
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000918 RunSVN(command + accumulated_paths,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000919 os.path.join(self._root_dir, self.relpath))
920 accumulated_paths = []
921 accumulated_length = 0
922 else:
923 accumulated_paths.append(p)
924 accumulated_length += len(p)
925 if accumulated_paths:
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000926 RunSVN(command + accumulated_paths,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000927 os.path.join(self._root_dir, self.relpath))
928
929 def status(self, options, args, file_list):
930 """Display status information."""
maruel@chromium.orgedd27d12009-05-01 17:46:56 +0000931 path = os.path.join(self._root_dir, self.relpath)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000932 command = ['status']
933 command.extend(args)
maruel@chromium.orgedd27d12009-05-01 17:46:56 +0000934 if not os.path.isdir(path):
935 # svn status won't work if the directory doesn't exist.
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000936 print("\n________ couldn't run \'%s\' in \'%s\':\nThe directory "
937 "does not exist."
938 % (' '.join(command), path))
maruel@chromium.orgedd27d12009-05-01 17:46:56 +0000939 # There's no file list to retrieve.
940 else:
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000941 RunSVNAndGetFileList(command, path, file_list)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000942
943
944## GClient implementation.
945
946
947class GClient(object):
948 """Object that represent a gclient checkout."""
949
950 supported_commands = [
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +0000951 'cleanup', 'diff', 'export', 'revert', 'status', 'update', 'runhooks'
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000952 ]
953
954 def __init__(self, root_dir, options):
955 self._root_dir = root_dir
956 self._options = options
957 self._config_content = None
958 self._config_dict = {}
959 self._deps_hooks = []
960
961 def SetConfig(self, content):
962 self._config_dict = {}
963 self._config_content = content
skylined@chromium.orgdf0032c2009-05-29 10:43:56 +0000964 try:
965 exec(content, self._config_dict)
966 except SyntaxError, e:
967 try:
968 # Try to construct a human readable error message
969 error_message = [
970 'There is a syntax error in your configuration file.',
971 'Line #%s, character %s:' % (e.lineno, e.offset),
972 '"%s"' % re.sub(r'[\r\n]*$', '', e.text) ]
973 except:
974 # Something went wrong, re-raise the original exception
975 raise e
976 else:
977 # Raise a new exception with the human readable message:
978 raise Error('\n'.join(error_message))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000979
980 def SaveConfig(self):
981 FileWrite(os.path.join(self._root_dir, self._options.config_filename),
982 self._config_content)
983
984 def _LoadConfig(self):
985 client_source = FileRead(os.path.join(self._root_dir,
986 self._options.config_filename))
987 self.SetConfig(client_source)
988
989 def ConfigContent(self):
990 return self._config_content
991
992 def GetVar(self, key, default=None):
993 return self._config_dict.get(key, default)
994
995 @staticmethod
996 def LoadCurrentConfig(options, from_dir=None):
997 """Searches for and loads a .gclient file relative to the current working
998 dir.
999
1000 Returns:
1001 A dict representing the contents of the .gclient file or an empty dict if
1002 the .gclient file doesn't exist.
1003 """
1004 if not from_dir:
1005 from_dir = os.curdir
1006 path = os.path.realpath(from_dir)
maruel@chromium.org0329e672009-05-13 18:41:04 +00001007 while not os.path.exists(os.path.join(path, options.config_filename)):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001008 next = os.path.split(path)
1009 if not next[1]:
1010 return None
1011 path = next[0]
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001012 client = GClient(path, options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001013 client._LoadConfig()
1014 return client
1015
1016 def SetDefaultConfig(self, solution_name, solution_url, safesync_url):
1017 self.SetConfig(DEFAULT_CLIENT_FILE_TEXT % (
1018 solution_name, solution_url, safesync_url
1019 ))
1020
1021 def _SaveEntries(self, entries):
1022 """Creates a .gclient_entries file to record the list of unique checkouts.
1023
1024 The .gclient_entries file lives in the same directory as .gclient.
1025
1026 Args:
1027 entries: A sequence of solution names.
1028 """
1029 text = "entries = [\n"
1030 for entry in entries:
1031 text += " \"%s\",\n" % entry
1032 text += "]\n"
1033 FileWrite(os.path.join(self._root_dir, self._options.entries_filename),
1034 text)
1035
1036 def _ReadEntries(self):
1037 """Read the .gclient_entries file for the given client.
1038
1039 Args:
1040 client: The client for which the entries file should be read.
1041
1042 Returns:
1043 A sequence of solution names, which will be empty if there is the
1044 entries file hasn't been created yet.
1045 """
1046 scope = {}
1047 filename = os.path.join(self._root_dir, self._options.entries_filename)
maruel@chromium.org0329e672009-05-13 18:41:04 +00001048 if not os.path.exists(filename):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001049 return []
1050 exec(FileRead(filename), scope)
1051 return scope["entries"]
1052
1053 class FromImpl:
1054 """Used to implement the From syntax."""
1055
1056 def __init__(self, module_name):
1057 self.module_name = module_name
1058
1059 def __str__(self):
1060 return 'From("%s")' % self.module_name
1061
1062 class _VarImpl:
1063 def __init__(self, custom_vars, local_scope):
1064 self._custom_vars = custom_vars
1065 self._local_scope = local_scope
1066
1067 def Lookup(self, var_name):
1068 """Implements the Var syntax."""
1069 if var_name in self._custom_vars:
1070 return self._custom_vars[var_name]
1071 elif var_name in self._local_scope.get("vars", {}):
1072 return self._local_scope["vars"][var_name]
1073 raise Error("Var is not defined: %s" % var_name)
1074
1075 def _ParseSolutionDeps(self, solution_name, solution_deps_content,
1076 custom_vars):
1077 """Parses the DEPS file for the specified solution.
1078
1079 Args:
1080 solution_name: The name of the solution to query.
1081 solution_deps_content: Content of the DEPS file for the solution
1082 custom_vars: A dict of vars to override any vars defined in the DEPS file.
1083
1084 Returns:
1085 A dict mapping module names (as relative paths) to URLs or an empty
1086 dict if the solution does not have a DEPS file.
1087 """
1088 # Skip empty
1089 if not solution_deps_content:
1090 return {}
1091 # Eval the content
1092 local_scope = {}
1093 var = self._VarImpl(custom_vars, local_scope)
1094 global_scope = {"From": self.FromImpl, "Var": var.Lookup, "deps_os": {}}
1095 exec(solution_deps_content, global_scope, local_scope)
1096 deps = local_scope.get("deps", {})
1097
1098 # load os specific dependencies if defined. these dependencies may
1099 # override or extend the values defined by the 'deps' member.
1100 if "deps_os" in local_scope:
1101 deps_os_choices = {
1102 "win32": "win",
1103 "win": "win",
1104 "cygwin": "win",
1105 "darwin": "mac",
1106 "mac": "mac",
1107 "unix": "unix",
1108 "linux": "unix",
1109 "linux2": "unix",
1110 }
1111
1112 if self._options.deps_os is not None:
1113 deps_to_include = self._options.deps_os.split(",")
1114 if "all" in deps_to_include:
1115 deps_to_include = deps_os_choices.values()
1116 else:
1117 deps_to_include = [deps_os_choices.get(self._options.platform, "unix")]
1118
1119 deps_to_include = set(deps_to_include)
1120 for deps_os_key in deps_to_include:
1121 os_deps = local_scope["deps_os"].get(deps_os_key, {})
1122 if len(deps_to_include) > 1:
1123 # Ignore any overrides when including deps for more than one
1124 # platform, so we collect the broadest set of dependencies available.
1125 # We may end up with the wrong revision of something for our
1126 # platform, but this is the best we can do.
1127 deps.update([x for x in os_deps.items() if not x[0] in deps])
1128 else:
1129 deps.update(os_deps)
1130
1131 if 'hooks' in local_scope:
1132 self._deps_hooks.extend(local_scope['hooks'])
1133
1134 # If use_relative_paths is set in the DEPS file, regenerate
1135 # the dictionary using paths relative to the directory containing
1136 # the DEPS file.
1137 if local_scope.get('use_relative_paths'):
1138 rel_deps = {}
1139 for d, url in deps.items():
1140 # normpath is required to allow DEPS to use .. in their
1141 # dependency local path.
1142 rel_deps[os.path.normpath(os.path.join(solution_name, d))] = url
1143 return rel_deps
1144 else:
1145 return deps
1146
1147 def _ParseAllDeps(self, solution_urls, solution_deps_content):
1148 """Parse the complete list of dependencies for the client.
1149
1150 Args:
1151 solution_urls: A dict mapping module names (as relative paths) to URLs
1152 corresponding to the solutions specified by the client. This parameter
1153 is passed as an optimization.
1154 solution_deps_content: A dict mapping module names to the content
1155 of their DEPS files
1156
1157 Returns:
1158 A dict mapping module names (as relative paths) to URLs corresponding
1159 to the entire set of dependencies to checkout for the given client.
1160
1161 Raises:
1162 Error: If a dependency conflicts with another dependency or of a solution.
1163 """
1164 deps = {}
1165 for solution in self.GetVar("solutions"):
1166 custom_vars = solution.get("custom_vars", {})
1167 solution_deps = self._ParseSolutionDeps(
1168 solution["name"],
1169 solution_deps_content[solution["name"]],
1170 custom_vars)
1171
1172 # If a line is in custom_deps, but not in the solution, we want to append
1173 # this line to the solution.
1174 if "custom_deps" in solution:
1175 for d in solution["custom_deps"]:
1176 if d not in solution_deps:
1177 solution_deps[d] = solution["custom_deps"][d]
1178
1179 for d in solution_deps:
1180 if "custom_deps" in solution and d in solution["custom_deps"]:
1181 # Dependency is overriden.
1182 url = solution["custom_deps"][d]
1183 if url is None:
1184 continue
1185 else:
1186 url = solution_deps[d]
1187 # if we have a From reference dependent on another solution, then
1188 # just skip the From reference. When we pull deps for the solution,
1189 # we will take care of this dependency.
1190 #
1191 # If multiple solutions all have the same From reference, then we
1192 # should only add one to our list of dependencies.
1193 if type(url) != str:
1194 if url.module_name in solution_urls:
1195 # Already parsed.
1196 continue
1197 if d in deps and type(deps[d]) != str:
1198 if url.module_name == deps[d].module_name:
1199 continue
1200 else:
1201 parsed_url = urlparse.urlparse(url)
1202 scheme = parsed_url[0]
1203 if not scheme:
1204 # A relative url. Fetch the real base.
1205 path = parsed_url[2]
1206 if path[0] != "/":
1207 raise Error(
1208 "relative DEPS entry \"%s\" must begin with a slash" % d)
1209 # Create a scm just to query the full url.
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001210 scm = SCMWrapper(solution["url"], self._root_dir, None)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001211 url = scm.FullUrlForRelativeUrl(url)
1212 if d in deps and deps[d] != url:
1213 raise Error(
1214 "Solutions have conflicting versions of dependency \"%s\"" % d)
1215 if d in solution_urls and solution_urls[d] != url:
1216 raise Error(
1217 "Dependency \"%s\" conflicts with specified solution" % d)
1218 # Grab the dependency.
1219 deps[d] = url
1220 return deps
1221
phajdan.jr@chromium.org71b40682009-07-31 23:40:09 +00001222 def _RunHookAction(self, hook_dict, matching_file_list):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001223 """Runs the action from a single hook.
1224 """
1225 command = hook_dict['action'][:]
1226 if command[0] == 'python':
1227 # If the hook specified "python" as the first item, the action is a
1228 # Python script. Run it by starting a new copy of the same
1229 # interpreter.
1230 command[0] = sys.executable
1231
phajdan.jr@chromium.org71b40682009-07-31 23:40:09 +00001232 if '$matching_files' in command:
phajdan.jr@chromium.org68f2e092009-08-06 17:05:35 +00001233 splice_index = command.index('$matching_files')
1234 command[splice_index:splice_index + 1] = matching_file_list
phajdan.jr@chromium.org71b40682009-07-31 23:40:09 +00001235
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001236 # Use a discrete exit status code of 2 to indicate that a hook action
1237 # failed. Users of this script may wish to treat hook action failures
1238 # differently from VC failures.
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +00001239 SubprocessCall(command, self._root_dir, fail_status=2)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001240
1241 def _RunHooks(self, command, file_list, is_using_git):
1242 """Evaluates all hooks, running actions as needed.
1243 """
1244 # Hooks only run for these command types.
1245 if not command in ('update', 'revert', 'runhooks'):
1246 return
1247
evan@chromium.org67820ef2009-07-27 17:23:00 +00001248 # Hooks only run when --nohooks is not specified
1249 if self._options.nohooks:
1250 return
1251
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001252 # Get any hooks from the .gclient file.
1253 hooks = self.GetVar("hooks", [])
1254 # Add any hooks found in DEPS files.
1255 hooks.extend(self._deps_hooks)
1256
1257 # If "--force" was specified, run all hooks regardless of what files have
1258 # changed. If the user is using git, then we don't know what files have
1259 # changed so we always run all hooks.
1260 if self._options.force or is_using_git:
1261 for hook_dict in hooks:
phajdan.jr@chromium.org71b40682009-07-31 23:40:09 +00001262 self._RunHookAction(hook_dict, [])
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001263 return
1264
1265 # Run hooks on the basis of whether the files from the gclient operation
1266 # match each hook's pattern.
1267 for hook_dict in hooks:
1268 pattern = re.compile(hook_dict['pattern'])
phajdan.jr@chromium.org71b40682009-07-31 23:40:09 +00001269 matching_file_list = [file for file in file_list if pattern.search(file)]
1270 if matching_file_list:
1271 self._RunHookAction(hook_dict, matching_file_list)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001272
1273 def RunOnDeps(self, command, args):
1274 """Runs a command on each dependency in a client and its dependencies.
1275
1276 The module's dependencies are specified in its top-level DEPS files.
1277
1278 Args:
1279 command: The command to use (e.g., 'status' or 'diff')
1280 args: list of str - extra arguments to add to the command line.
1281
1282 Raises:
1283 Error: If the client has conflicting entries.
1284 """
1285 if not command in self.supported_commands:
1286 raise Error("'%s' is an unsupported command" % command)
1287
1288 # Check for revision overrides.
1289 revision_overrides = {}
1290 for revision in self._options.revisions:
1291 if revision.find("@") == -1:
1292 raise Error(
1293 "Specify the full dependency when specifying a revision number.")
1294 revision_elem = revision.split("@")
1295 # Disallow conflicting revs
1296 if revision_overrides.has_key(revision_elem[0]) and \
1297 revision_overrides[revision_elem[0]] != revision_elem[1]:
1298 raise Error(
1299 "Conflicting revision numbers specified.")
1300 revision_overrides[revision_elem[0]] = revision_elem[1]
1301
1302 solutions = self.GetVar("solutions")
1303 if not solutions:
1304 raise Error("No solution specified")
1305
1306 # When running runhooks --force, there's no need to consult the SCM.
1307 # All known hooks are expected to run unconditionally regardless of working
1308 # copy state, so skip the SCM status check.
1309 run_scm = not (command == 'runhooks' and self._options.force)
1310
1311 entries = {}
1312 entries_deps_content = {}
1313 file_list = []
1314 # Run on the base solutions first.
1315 for solution in solutions:
1316 name = solution["name"]
1317 if name in entries:
1318 raise Error("solution %s specified more than once" % name)
1319 url = solution["url"]
1320 entries[name] = url
1321 if run_scm:
1322 self._options.revision = revision_overrides.get(name)
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001323 scm = SCMWrapper(url, self._root_dir, name)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001324 scm.RunCommand(command, self._options, args, file_list)
phajdan.jr@chromium.orgd83b2b22009-08-11 15:30:55 +00001325 file_list = [os.path.join(name, file.strip()) for file in file_list]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001326 self._options.revision = None
1327 try:
1328 deps_content = FileRead(os.path.join(self._root_dir, name,
1329 self._options.deps_file))
1330 except IOError, e:
1331 if e.errno != errno.ENOENT:
1332 raise
1333 deps_content = ""
1334 entries_deps_content[name] = deps_content
1335
1336 # Process the dependencies next (sort alphanumerically to ensure that
1337 # containing directories get populated first and for readability)
1338 deps = self._ParseAllDeps(entries, entries_deps_content)
1339 deps_to_process = deps.keys()
1340 deps_to_process.sort()
1341
1342 # First pass for direct dependencies.
1343 for d in deps_to_process:
1344 if type(deps[d]) == str:
1345 url = deps[d]
1346 entries[d] = url
1347 if run_scm:
1348 self._options.revision = revision_overrides.get(d)
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001349 scm = SCMWrapper(url, self._root_dir, d)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001350 scm.RunCommand(command, self._options, args, file_list)
1351 self._options.revision = None
1352
1353 # Second pass for inherited deps (via the From keyword)
1354 for d in deps_to_process:
1355 if type(deps[d]) != str:
1356 sub_deps = self._ParseSolutionDeps(
1357 deps[d].module_name,
1358 FileRead(os.path.join(self._root_dir,
1359 deps[d].module_name,
1360 self._options.deps_file)),
1361 {})
1362 url = sub_deps[d]
1363 entries[d] = url
1364 if run_scm:
1365 self._options.revision = revision_overrides.get(d)
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001366 scm = SCMWrapper(url, self._root_dir, d)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001367 scm.RunCommand(command, self._options, args, file_list)
1368 self._options.revision = None
phajdan.jr@chromium.orgd83b2b22009-08-11 15:30:55 +00001369
1370 # Convert all absolute paths to relative.
1371 for i in range(len(file_list)):
1372 # TODO(phajdan.jr): We should know exactly when the paths are absolute.
1373 # It depends on the command being executed (like runhooks vs sync).
1374 if not os.path.isabs(file_list[i]):
1375 continue
1376
1377 prefix = os.path.commonprefix([self._root_dir.lower(),
1378 file_list[i].lower()])
1379 file_list[i] = file_list[i][len(prefix):]
1380
1381 # Strip any leading path separators.
1382 while file_list[i].startswith('\\') or file_list[i].startswith('/'):
1383 file_list[i] = file_list[i][1:]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001384
1385 is_using_git = IsUsingGit(self._root_dir, entries.keys())
1386 self._RunHooks(command, file_list, is_using_git)
1387
1388 if command == 'update':
ajwong@chromium.orgcdcee802009-06-23 15:30:42 +00001389 # Notify the user if there is an orphaned entry in their working copy.
1390 # Only delete the directory if there are no changes in it, and
1391 # delete_unversioned_trees is set to true.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001392 prev_entries = self._ReadEntries()
1393 for entry in prev_entries:
maruel@chromium.orgc5e9aec2009-08-03 18:25:56 +00001394 # Fix path separator on Windows.
1395 entry_fixed = entry.replace('/', os.path.sep)
1396 e_dir = os.path.join(self._root_dir, entry_fixed)
1397 # Use entry and not entry_fixed there.
maruel@chromium.org0329e672009-05-13 18:41:04 +00001398 if entry not in entries and os.path.exists(e_dir):
ajwong@chromium.org8399dc02009-06-23 21:36:25 +00001399 if not self._options.delete_unversioned_trees or \
1400 CaptureSVNStatus(e_dir):
maruel@chromium.orgc5e9aec2009-08-03 18:25:56 +00001401 # There are modified files in this entry. Keep warning until
1402 # removed.
1403 entries[entry] = None
1404 print(("\nWARNING: \"%s\" is no longer part of this client. "
1405 "It is recommended that you manually remove it.\n") %
1406 entry_fixed)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001407 else:
1408 # Delete the entry
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +00001409 print("\n________ deleting \'%s\' " +
maruel@chromium.orgc5e9aec2009-08-03 18:25:56 +00001410 "in \'%s\'") % (entry_fixed, self._root_dir)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001411 RemoveDirectory(e_dir)
1412 # record the current list of entries for next time
1413 self._SaveEntries(entries)
1414
1415 def PrintRevInfo(self):
1416 """Output revision info mapping for the client and its dependencies. This
1417 allows the capture of a overall "revision" for the source tree that can
1418 be used to reproduce the same tree in the future. The actual output
1419 contains enough information (source paths, svn server urls and revisions)
1420 that it can be used either to generate external svn commands (without
1421 gclient) or as input to gclient's --rev option (with some massaging of
1422 the data).
1423
1424 NOTE: Unlike RunOnDeps this does not require a local checkout and is run
1425 on the Pulse master. It MUST NOT execute hooks.
1426
1427 Raises:
1428 Error: If the client has conflicting entries.
1429 """
1430 # Check for revision overrides.
1431 revision_overrides = {}
1432 for revision in self._options.revisions:
1433 if revision.find("@") < 0:
1434 raise Error(
1435 "Specify the full dependency when specifying a revision number.")
1436 revision_elem = revision.split("@")
1437 # Disallow conflicting revs
1438 if revision_overrides.has_key(revision_elem[0]) and \
1439 revision_overrides[revision_elem[0]] != revision_elem[1]:
1440 raise Error(
1441 "Conflicting revision numbers specified.")
1442 revision_overrides[revision_elem[0]] = revision_elem[1]
1443
1444 solutions = self.GetVar("solutions")
1445 if not solutions:
1446 raise Error("No solution specified")
1447
1448 entries = {}
1449 entries_deps_content = {}
1450
1451 # Inner helper to generate base url and rev tuple (including honoring
1452 # |revision_overrides|)
1453 def GetURLAndRev(name, original_url):
1454 if original_url.find("@") < 0:
1455 if revision_overrides.has_key(name):
1456 return (original_url, int(revision_overrides[name]))
1457 else:
1458 # TODO(aharper): SVN/SCMWrapper cleanup (non-local commandset)
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +00001459 return (original_url, CaptureSVNHeadRevision(original_url))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001460 else:
1461 url_components = original_url.split("@")
1462 if revision_overrides.has_key(name):
1463 return (url_components[0], int(revision_overrides[name]))
1464 else:
1465 return (url_components[0], int(url_components[1]))
1466
1467 # Run on the base solutions first.
1468 for solution in solutions:
1469 name = solution["name"]
1470 if name in entries:
1471 raise Error("solution %s specified more than once" % name)
1472 (url, rev) = GetURLAndRev(name, solution["url"])
1473 entries[name] = "%s@%d" % (url, rev)
1474 # TODO(aharper): SVN/SCMWrapper cleanup (non-local commandset)
1475 entries_deps_content[name] = CaptureSVN(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001476 ["cat",
1477 "%s/%s@%d" % (url,
1478 self._options.deps_file,
1479 rev)],
1480 os.getcwd())
1481
1482 # Process the dependencies next (sort alphanumerically to ensure that
1483 # containing directories get populated first and for readability)
1484 deps = self._ParseAllDeps(entries, entries_deps_content)
1485 deps_to_process = deps.keys()
1486 deps_to_process.sort()
1487
1488 # First pass for direct dependencies.
1489 for d in deps_to_process:
1490 if type(deps[d]) == str:
1491 (url, rev) = GetURLAndRev(d, deps[d])
1492 entries[d] = "%s@%d" % (url, rev)
1493
1494 # Second pass for inherited deps (via the From keyword)
1495 for d in deps_to_process:
1496 if type(deps[d]) != str:
1497 deps_parent_url = entries[deps[d].module_name]
1498 if deps_parent_url.find("@") < 0:
1499 raise Error("From %s missing revisioned url" % deps[d].module_name)
1500 deps_parent_url_components = deps_parent_url.split("@")
1501 # TODO(aharper): SVN/SCMWrapper cleanup (non-local commandset)
1502 deps_parent_content = CaptureSVN(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001503 ["cat",
1504 "%s/%s@%s" % (deps_parent_url_components[0],
1505 self._options.deps_file,
1506 deps_parent_url_components[1])],
1507 os.getcwd())
1508 sub_deps = self._ParseSolutionDeps(
1509 deps[d].module_name,
1510 FileRead(os.path.join(self._root_dir,
1511 deps[d].module_name,
1512 self._options.deps_file)),
1513 {})
1514 (url, rev) = GetURLAndRev(d, sub_deps[d])
1515 entries[d] = "%s@%d" % (url, rev)
maruel@chromium.org57e893e2009-08-19 18:12:09 +00001516 print(";\n\n".join(["%s: %s" % (x, entries[x])
1517 for x in sorted(entries.keys())]))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001518
1519
1520## gclient commands.
1521
1522
1523def DoCleanup(options, args):
1524 """Handle the cleanup subcommand.
1525
1526 Raises:
1527 Error: if client isn't configured properly.
1528 """
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001529 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001530 if not client:
1531 raise Error("client not configured; see 'gclient config'")
1532 if options.verbose:
1533 # Print out the .gclient file. This is longer than if we just printed the
1534 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +00001535 print(client.ConfigContent())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001536 options.verbose = True
1537 return client.RunOnDeps('cleanup', args)
1538
1539
1540def DoConfig(options, args):
1541 """Handle the config subcommand.
1542
1543 Args:
1544 options: If options.spec set, a string providing contents of config file.
1545 args: The command line args. If spec is not set,
1546 then args[0] is a string URL to get for config file.
1547
1548 Raises:
1549 Error: on usage error
1550 """
1551 if len(args) < 1 and not options.spec:
1552 raise Error("required argument missing; see 'gclient help config'")
maruel@chromium.org0329e672009-05-13 18:41:04 +00001553 if os.path.exists(options.config_filename):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001554 raise Error("%s file already exists in the current directory" %
1555 options.config_filename)
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001556 client = GClient('.', options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001557 if options.spec:
1558 client.SetConfig(options.spec)
1559 else:
1560 # TODO(darin): it would be nice to be able to specify an alternate relpath
1561 # for the given URL.
maruel@chromium.org1ab7ffc2009-06-03 17:21:37 +00001562 base_url = args[0].rstrip('/')
1563 name = base_url.split("/")[-1]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001564 safesync_url = ""
1565 if len(args) > 1:
1566 safesync_url = args[1]
1567 client.SetDefaultConfig(name, base_url, safesync_url)
1568 client.SaveConfig()
1569
1570
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +00001571def DoExport(options, args):
1572 """Handle the export subcommand.
phajdan.jr@chromium.org71b40682009-07-31 23:40:09 +00001573
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +00001574 Raises:
1575 Error: on usage error
1576 """
1577 if len(args) != 1:
1578 raise Error("Need directory name")
1579 client = GClient.LoadCurrentConfig(options)
1580
1581 if not client:
1582 raise Error("client not configured; see 'gclient config'")
1583
1584 if options.verbose:
1585 # Print out the .gclient file. This is longer than if we just printed the
1586 # client dict, but more legible, and it might contain helpful comments.
1587 print(client.ConfigContent())
1588 return client.RunOnDeps('export', args)
1589
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001590def DoHelp(options, args):
1591 """Handle the help subcommand giving help for another subcommand.
1592
1593 Raises:
1594 Error: if the command is unknown.
1595 """
1596 if len(args) == 1 and args[0] in COMMAND_USAGE_TEXT:
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +00001597 print(COMMAND_USAGE_TEXT[args[0]])
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001598 else:
1599 raise Error("unknown subcommand '%s'; see 'gclient help'" % args[0])
1600
1601
1602def DoStatus(options, args):
1603 """Handle the status subcommand.
1604
1605 Raises:
1606 Error: if client isn't configured properly.
1607 """
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001608 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001609 if not client:
1610 raise Error("client not configured; see 'gclient config'")
1611 if options.verbose:
1612 # Print out the .gclient file. This is longer than if we just printed the
1613 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +00001614 print(client.ConfigContent())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001615 options.verbose = True
1616 return client.RunOnDeps('status', args)
1617
1618
1619def DoUpdate(options, args):
1620 """Handle the update and sync subcommands.
1621
1622 Raises:
1623 Error: if client isn't configured properly.
1624 """
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001625 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001626
1627 if not client:
1628 raise Error("client not configured; see 'gclient config'")
1629
1630 if not options.head:
1631 solutions = client.GetVar('solutions')
1632 if solutions:
1633 for s in solutions:
1634 if s.get('safesync_url', ''):
1635 # rip through revisions and make sure we're not over-riding
1636 # something that was explicitly passed
1637 has_key = False
1638 for r in options.revisions:
1639 if r.split('@')[0] == s['name']:
1640 has_key = True
1641 break
1642
1643 if not has_key:
1644 handle = urllib.urlopen(s['safesync_url'])
1645 rev = handle.read().strip()
1646 handle.close()
1647 if len(rev):
1648 options.revisions.append(s['name']+'@'+rev)
1649
1650 if options.verbose:
1651 # Print out the .gclient file. This is longer than if we just printed the
1652 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +00001653 print(client.ConfigContent())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001654 return client.RunOnDeps('update', args)
1655
1656
1657def DoDiff(options, args):
1658 """Handle the diff subcommand.
1659
1660 Raises:
1661 Error: if client isn't configured properly.
1662 """
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001663 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001664 if not client:
1665 raise Error("client not configured; see 'gclient config'")
1666 if options.verbose:
1667 # Print out the .gclient file. This is longer than if we just printed the
1668 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +00001669 print(client.ConfigContent())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001670 options.verbose = True
1671 return client.RunOnDeps('diff', args)
1672
1673
1674def DoRevert(options, args):
1675 """Handle the revert subcommand.
1676
1677 Raises:
1678 Error: if client isn't configured properly.
1679 """
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001680 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001681 if not client:
1682 raise Error("client not configured; see 'gclient config'")
1683 return client.RunOnDeps('revert', args)
1684
1685
1686def DoRunHooks(options, args):
1687 """Handle the runhooks subcommand.
1688
1689 Raises:
1690 Error: if client isn't configured properly.
1691 """
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001692 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001693 if not client:
1694 raise Error("client not configured; see 'gclient config'")
1695 if options.verbose:
1696 # Print out the .gclient file. This is longer than if we just printed the
1697 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +00001698 print(client.ConfigContent())
maruel@chromium.org5df6a462009-08-28 18:52:26 +00001699 options.force = True
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001700 return client.RunOnDeps('runhooks', args)
1701
1702
1703def DoRevInfo(options, args):
1704 """Handle the revinfo subcommand.
1705
1706 Raises:
1707 Error: if client isn't configured properly.
1708 """
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001709 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001710 if not client:
1711 raise Error("client not configured; see 'gclient config'")
1712 client.PrintRevInfo()
1713
1714
1715gclient_command_map = {
1716 "cleanup": DoCleanup,
1717 "config": DoConfig,
1718 "diff": DoDiff,
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +00001719 "export": DoExport,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001720 "help": DoHelp,
1721 "status": DoStatus,
1722 "sync": DoUpdate,
1723 "update": DoUpdate,
1724 "revert": DoRevert,
1725 "runhooks": DoRunHooks,
1726 "revinfo" : DoRevInfo,
1727}
1728
1729
1730def DispatchCommand(command, options, args, command_map=None):
1731 """Dispatches the appropriate subcommand based on command line arguments."""
1732 if command_map is None:
1733 command_map = gclient_command_map
1734
1735 if command in command_map:
1736 return command_map[command](options, args)
1737 else:
1738 raise Error("unknown subcommand '%s'; see 'gclient help'" % command)
1739
1740
1741def Main(argv):
1742 """Parse command line arguments and dispatch command."""
1743
1744 option_parser = optparse.OptionParser(usage=DEFAULT_USAGE_TEXT,
1745 version=__version__)
1746 option_parser.disable_interspersed_args()
1747 option_parser.add_option("", "--force", action="store_true", default=False,
1748 help=("(update/sync only) force update even "
1749 "for modules which haven't changed"))
evan@chromium.org67820ef2009-07-27 17:23:00 +00001750 option_parser.add_option("", "--nohooks", action="store_true", default=False,
1751 help=("(update/sync/revert only) prevent the hooks from "
1752 "running"))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001753 option_parser.add_option("", "--revision", action="append", dest="revisions",
1754 metavar="REV", default=[],
1755 help=("(update/sync only) sync to a specific "
1756 "revision, can be used multiple times for "
1757 "each solution, e.g. --revision=src@123, "
1758 "--revision=internal@32"))
1759 option_parser.add_option("", "--deps", default=None, dest="deps_os",
1760 metavar="OS_LIST",
1761 help=("(update/sync only) sync deps for the "
1762 "specified (comma-separated) platform(s); "
1763 "'all' will sync all platforms"))
1764 option_parser.add_option("", "--spec", default=None,
1765 help=("(config only) create a gclient file "
1766 "containing the provided string"))
1767 option_parser.add_option("", "--verbose", action="store_true", default=False,
1768 help="produce additional output for diagnostics")
1769 option_parser.add_option("", "--manually_grab_svn_rev", action="store_true",
1770 default=False,
1771 help="Skip svn up whenever possible by requesting "
1772 "actual HEAD revision from the repository")
1773 option_parser.add_option("", "--head", action="store_true", default=False,
1774 help=("skips any safesync_urls specified in "
1775 "configured solutions"))
ajwong@chromium.orgcdcee802009-06-23 15:30:42 +00001776 option_parser.add_option("", "--delete_unversioned_trees",
1777 action="store_true", default=False,
1778 help=("on update, delete any unexpected "
1779 "unversioned trees that are in the checkout"))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001780
1781 if len(argv) < 2:
1782 # Users don't need to be told to use the 'help' command.
1783 option_parser.print_help()
1784 return 1
1785 # Add manual support for --version as first argument.
1786 if argv[1] == '--version':
1787 option_parser.print_version()
1788 return 0
1789
1790 # Add manual support for --help as first argument.
1791 if argv[1] == '--help':
1792 argv[1] = 'help'
1793
1794 command = argv[1]
1795 options, args = option_parser.parse_args(argv[2:])
1796
1797 if len(argv) < 3 and command == "help":
1798 option_parser.print_help()
1799 return 0
1800
1801 # Files used for configuration and state saving.
1802 options.config_filename = os.environ.get("GCLIENT_FILE", ".gclient")
1803 options.entries_filename = ".gclient_entries"
1804 options.deps_file = "DEPS"
1805
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001806 options.platform = sys.platform
1807 return DispatchCommand(command, options, args)
1808
1809
1810if "__main__" == __name__:
1811 try:
1812 result = Main(sys.argv)
1813 except Error, e:
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +00001814 print >> sys.stderr, "Error: %s" % str(e)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001815 result = 1
1816 sys.exit(result)
1817
1818# vim: ts=2:sw=2:tw=80:et: