blob: 9dbfcedf936576d7e0f3b0d7f90577906c61e7f0 [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
42 be run based on what files have been modified in the working copy
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000043 with the "runhooks" operation. If any of these operation are run with
44 --force, all known hooks will run regardless of the state of the working
45 copy.
46
47 Each item in a "hooks" list is a dict, containing these two keys:
48 "pattern" The associated value is a string containing a regular
49 expression. When a file whose pathname matches the expression
50 is checked out, updated, or reverted, the hook's "action" will
51 run.
52 "action" A list describing a command to run along with its arguments, if
53 any. An action command will run at most one time per gclient
54 invocation, regardless of how many files matched the pattern.
55 The action is executed in the same directory as the .gclient
56 file. If the first item in the list is the string "python",
57 the current Python interpreter (sys.executable) will be used
58 to run the command.
59
60 Example:
61 hooks = [
62 { "pattern": "\\.(gif|jpe?g|pr0n|png)$",
63 "action": ["python", "image_indexer.py", "--all"]},
64 ]
65"""
66
67__author__ = "darinf@gmail.com (Darin Fisher)"
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +000068__version__ = "0.3.2"
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000069
70import errno
71import optparse
72import os
73import re
74import stat
75import subprocess
76import sys
77import time
78import urlparse
79import xml.dom.minidom
80import urllib
81
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000082
83SVN_COMMAND = "svn"
84
85
86# default help text
87DEFAULT_USAGE_TEXT = (
88"""usage: %prog <subcommand> [options] [--] [svn options/args...]
89a wrapper for managing a set of client modules in svn.
90Version """ + __version__ + """
91
92subcommands:
93 cleanup
94 config
95 diff
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +000096 export
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000097 revert
98 status
99 sync
100 update
101 runhooks
102 revinfo
103
104Options and extra arguments can be passed to invoked svn commands by
105appending them to the command line. Note that if the first such
106appended option starts with a dash (-) then the options must be
107preceded by -- to distinguish them from gclient options.
108
109For additional help on a subcommand or examples of usage, try
110 %prog help <subcommand>
111 %prog help files
112""")
113
114GENERIC_UPDATE_USAGE_TEXT = (
115 """Perform a checkout/update of the modules specified by the gclient
116configuration; see 'help config'. Unless --revision is specified,
117then the latest revision of the root solutions is checked out, with
118dependent submodule versions updated according to DEPS files.
119If --revision is specified, then the given revision is used in place
120of the latest, either for a single solution or for all solutions.
121Unless the --force option is provided, solutions and modules whose
122local revision matches the one to update (i.e., they have not changed
evan@chromium.org67820ef2009-07-27 17:23:00 +0000123in the repository) are *not* modified. Unless --nohooks is provided,
124the hooks are run.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000125This a synonym for 'gclient %(alias)s'
126
127usage: gclient %(cmd)s [options] [--] [svn update options/args]
128
129Valid options:
130 --force : force update even for unchanged modules
evan@chromium.org67820ef2009-07-27 17:23:00 +0000131 --nohooks : don't run the hooks after the update is complete
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000132 --revision REV : update/checkout all solutions with specified revision
133 --revision SOLUTION@REV : update given solution to specified revision
134 --deps PLATFORM(S) : sync deps for the given platform(s), or 'all'
135 --verbose : output additional diagnostics
maruel@chromium.orgb8b6b872009-06-30 18:50:56 +0000136 --head : update to latest revision, instead of last good revision
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000137
138Examples:
139 gclient %(cmd)s
140 update files from SVN according to current configuration,
141 *for modules which have changed since last update or sync*
142 gclient %(cmd)s --force
143 update files from SVN according to current configuration, for
144 all modules (useful for recovering files deleted from local copy)
145""")
146
147COMMAND_USAGE_TEXT = {
148 "cleanup":
149 """Clean up all working copies, using 'svn cleanup' for each module.
150Additional options and args may be passed to 'svn cleanup'.
151
152usage: cleanup [options] [--] [svn cleanup args/options]
153
154Valid options:
155 --verbose : output additional diagnostics
156""",
157 "config": """Create a .gclient file in the current directory; this
158specifies the configuration for further commands. After update/sync,
159top-level DEPS files in each module are read to determine dependent
160modules to operate on as well. If optional [url] parameter is
161provided, then configuration is read from a specified Subversion server
162URL. Otherwise, a --spec option must be provided.
163
164usage: config [option | url] [safesync url]
165
166Valid options:
167 --spec=GCLIENT_SPEC : contents of .gclient are read from string parameter.
168 *Note that due to Cygwin/Python brokenness, it
169 probably can't contain any newlines.*
170
171Examples:
172 gclient config https://gclient.googlecode.com/svn/trunk/gclient
173 configure a new client to check out gclient.py tool sources
174 gclient config --spec='solutions=[{"name":"gclient","""
175 '"url":"https://gclient.googlecode.com/svn/trunk/gclient",'
176 '"custom_deps":{}}]',
177 "diff": """Display the differences between two revisions of modules.
178(Does 'svn diff' for each checked out module and dependences.)
179Additional args and options to 'svn diff' can be passed after
180gclient options.
181
182usage: diff [options] [--] [svn args/options]
183
184Valid options:
185 --verbose : output additional diagnostics
186
187Examples:
188 gclient diff
189 simple 'svn diff' for configured client and dependences
190 gclient diff -- -x -b
191 use 'svn diff -x -b' to suppress whitespace-only differences
192 gclient diff -- -r HEAD -x -b
193 diff versus the latest version of each module
194""",
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +0000195 "export":
196 """Wrapper for svn export for all managed directories
197""",
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000198 "revert":
199 """Revert every file in every managed directory in the client view.
200
201usage: revert
202""",
203 "status":
204 """Show the status of client and dependent modules, using 'svn diff'
205for each module. Additional options and args may be passed to 'svn diff'.
206
207usage: status [options] [--] [svn diff args/options]
208
209Valid options:
210 --verbose : output additional diagnostics
evan@chromium.org67820ef2009-07-27 17:23:00 +0000211 --nohooks : don't run the hooks after the update is complete
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000212""",
213 "sync": GENERIC_UPDATE_USAGE_TEXT % {"cmd": "sync", "alias": "update"},
214 "update": GENERIC_UPDATE_USAGE_TEXT % {"cmd": "update", "alias": "sync"},
215 "help": """Describe the usage of this program or its subcommands.
216
217usage: help [options] [subcommand]
218
219Valid options:
220 --verbose : output additional diagnostics
221""",
222 "runhooks":
223 """Runs hooks for files that have been modified in the local working copy,
224according to 'svn status'.
225
226usage: runhooks [options]
227
228Valid options:
229 --force : runs all known hooks, regardless of the working
230 copy status
231 --verbose : output additional diagnostics
232""",
233 "revinfo":
234 """Outputs source path, server URL and revision information for every
235dependency in all solutions (no local checkout required).
236
237usage: revinfo [options]
238""",
239}
240
241# parameterized by (solution_name, solution_url, safesync_url)
242DEFAULT_CLIENT_FILE_TEXT = (
243 """
244# An element of this array (a \"solution\") describes a repository directory
245# that will be checked out into your working copy. Each solution may
246# optionally define additional dependencies (via its DEPS file) to be
247# checked out alongside the solution's directory. A solution may also
248# specify custom dependencies (via the \"custom_deps\" property) that
249# override or augment the dependencies specified by the DEPS file.
250# If a \"safesync_url\" is specified, it is assumed to reference the location of
251# a text file which contains nothing but the last known good SCM revision to
252# sync against. It is fetched if specified and used unless --head is passed
253solutions = [
254 { \"name\" : \"%s\",
255 \"url\" : \"%s\",
256 \"custom_deps\" : {
257 # To use the trunk of a component instead of what's in DEPS:
258 #\"component\": \"https://svnserver/component/trunk/\",
259 # To exclude a component from your working copy:
260 #\"data/really_large_component\": None,
261 },
262 \"safesync_url\": \"%s\"
263 }
264]
265""")
266
267
268## Generic utils
269
maruel@chromium.orge105d8d2009-04-30 17:58:25 +0000270def ParseXML(output):
271 try:
272 return xml.dom.minidom.parseString(output)
273 except xml.parsers.expat.ExpatError:
274 return None
275
276
maruel@chromium.org483b0082009-05-07 02:57:14 +0000277def GetNamedNodeText(node, node_name):
278 child_nodes = node.getElementsByTagName(node_name)
279 if not child_nodes:
280 return None
281 assert len(child_nodes) == 1 and child_nodes[0].childNodes.length == 1
282 return child_nodes[0].firstChild.nodeValue
283
284
285def GetNodeNamedAttributeText(node, node_name, attribute_name):
286 child_nodes = node.getElementsByTagName(node_name)
287 if not child_nodes:
288 return None
289 assert len(child_nodes) == 1
290 return child_nodes[0].getAttribute(attribute_name)
291
292
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000293class Error(Exception):
294 """gclient exception class."""
295 pass
296
297class PrintableObject(object):
298 def __str__(self):
299 output = ''
300 for i in dir(self):
301 if i.startswith('__'):
302 continue
303 output += '%s = %s\n' % (i, str(getattr(self, i, '')))
304 return output
305
306
307def FileRead(filename):
308 content = None
309 f = open(filename, "rU")
310 try:
311 content = f.read()
312 finally:
313 f.close()
314 return content
315
316
317def FileWrite(filename, content):
318 f = open(filename, "w")
319 try:
320 f.write(content)
321 finally:
322 f.close()
323
324
325def RemoveDirectory(*path):
326 """Recursively removes a directory, even if it's marked read-only.
327
328 Remove the directory located at *path, if it exists.
329
330 shutil.rmtree() doesn't work on Windows if any of the files or directories
331 are read-only, which svn repositories and some .svn files are. We need to
332 be able to force the files to be writable (i.e., deletable) as we traverse
333 the tree.
334
335 Even with all this, Windows still sometimes fails to delete a file, citing
336 a permission error (maybe something to do with antivirus scans or disk
337 indexing). The best suggestion any of the user forums had was to wait a
338 bit and try again, so we do that too. It's hand-waving, but sometimes it
339 works. :/
340
341 On POSIX systems, things are a little bit simpler. The modes of the files
342 to be deleted doesn't matter, only the modes of the directories containing
343 them are significant. As the directory tree is traversed, each directory
344 has its mode set appropriately before descending into it. This should
345 result in the entire tree being removed, with the possible exception of
346 *path itself, because nothing attempts to change the mode of its parent.
347 Doing so would be hazardous, as it's not a directory slated for removal.
348 In the ordinary case, this is not a problem: for our purposes, the user
349 will never lack write permission on *path's parent.
350 """
351 file_path = os.path.join(*path)
352 if not os.path.exists(file_path):
353 return
354
355 if os.path.islink(file_path) or not os.path.isdir(file_path):
356 raise Error("RemoveDirectory asked to remove non-directory %s" % file_path)
357
358 has_win32api = False
359 if sys.platform == 'win32':
360 has_win32api = True
361 # Some people don't have the APIs installed. In that case we'll do without.
362 try:
363 win32api = __import__('win32api')
364 win32con = __import__('win32con')
365 except ImportError:
366 has_win32api = False
367 else:
368 # On POSIX systems, we need the x-bit set on the directory to access it,
369 # the r-bit to see its contents, and the w-bit to remove files from it.
370 # The actual modes of the files within the directory is irrelevant.
371 os.chmod(file_path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
372 for fn in os.listdir(file_path):
373 fullpath = os.path.join(file_path, fn)
374
375 # If fullpath is a symbolic link that points to a directory, isdir will
376 # be True, but we don't want to descend into that as a directory, we just
377 # want to remove the link. Check islink and treat links as ordinary files
378 # would be treated regardless of what they reference.
379 if os.path.islink(fullpath) or not os.path.isdir(fullpath):
380 if sys.platform == 'win32':
381 os.chmod(fullpath, stat.S_IWRITE)
382 if has_win32api:
383 win32api.SetFileAttributes(fullpath, win32con.FILE_ATTRIBUTE_NORMAL)
384 try:
385 os.remove(fullpath)
386 except OSError, e:
387 if e.errno != errno.EACCES or sys.platform != 'win32':
388 raise
389 print 'Failed to delete %s: trying again' % fullpath
390 time.sleep(0.1)
391 os.remove(fullpath)
392 else:
393 RemoveDirectory(fullpath)
394
395 if sys.platform == 'win32':
396 os.chmod(file_path, stat.S_IWRITE)
397 if has_win32api:
398 win32api.SetFileAttributes(file_path, win32con.FILE_ATTRIBUTE_NORMAL)
399 try:
400 os.rmdir(file_path)
401 except OSError, e:
402 if e.errno != errno.EACCES or sys.platform != 'win32':
403 raise
404 print 'Failed to remove %s: trying again' % file_path
405 time.sleep(0.1)
406 os.rmdir(file_path)
407
408
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000409def SubprocessCall(command, in_directory, fail_status=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000410 """Runs command, a list, in directory in_directory.
411
412 This function wraps SubprocessCallAndCapture, but does not perform the
413 capturing functions. See that function for a more complete usage
414 description.
415 """
416 # Call subprocess and capture nothing:
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000417 SubprocessCallAndCapture(command, in_directory, fail_status)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000418
419
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000420def SubprocessCallAndCapture(command, in_directory, fail_status=None,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000421 pattern=None, capture_list=None):
422 """Runs command, a list, in directory in_directory.
423
424 A message indicating what is being done, as well as the command's stdout,
425 is printed to out.
426
427 If a pattern is specified, any line in the output matching pattern will have
428 its first match group appended to capture_list.
429
430 If the command fails, as indicated by a nonzero exit status, gclient will
431 exit with an exit status of fail_status. If fail_status is None (the
432 default), gclient will raise an Error exception.
433 """
434
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000435 print("\n________ running \'%s\' in \'%s\'"
436 % (' '.join(command), in_directory))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000437
438 # *Sigh*: Windows needs shell=True, or else it won't search %PATH% for the
439 # executable, but shell=True makes subprocess on Linux fail when it's called
440 # with a list because it only tries to execute the first item in the list.
441 kid = subprocess.Popen(command, bufsize=0, cwd=in_directory,
442 shell=(sys.platform == 'win32'), stdout=subprocess.PIPE)
443
444 if pattern:
445 compiled_pattern = re.compile(pattern)
446
447 # Also, we need to forward stdout to prevent weird re-ordering of output.
448 # This has to be done on a per byte basis to make sure it is not buffered:
449 # normally buffering is done for each line, but if svn requests input, no
450 # end-of-line character is output after the prompt and it would not show up.
451 in_byte = kid.stdout.read(1)
452 in_line = ""
453 while in_byte:
454 if in_byte != "\r":
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000455 sys.stdout.write(in_byte)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000456 in_line += in_byte
457 if in_byte == "\n" and pattern:
458 match = compiled_pattern.search(in_line[:-1])
459 if match:
460 capture_list.append(match.group(1))
461 in_line = ""
462 in_byte = kid.stdout.read(1)
463 rv = kid.wait()
464
465 if rv:
466 msg = "failed to run command: %s" % " ".join(command)
467
468 if fail_status != None:
469 print >>sys.stderr, msg
470 sys.exit(fail_status)
471
472 raise Error(msg)
473
474
475def IsUsingGit(root, paths):
476 """Returns True if we're using git to manage any of our checkouts.
477 |entries| is a list of paths to check."""
478 for path in paths:
479 if os.path.exists(os.path.join(root, path, '.git')):
480 return True
481 return False
482
483# -----------------------------------------------------------------------------
484# SVN utils:
485
486
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000487def RunSVN(args, in_directory):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000488 """Runs svn, sending output to stdout.
489
490 Args:
491 args: A sequence of command line parameters to be passed to svn.
492 in_directory: The directory where svn is to be run.
493
494 Raises:
495 Error: An error occurred while running the svn command.
496 """
497 c = [SVN_COMMAND]
498 c.extend(args)
499
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000500 SubprocessCall(c, in_directory)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000501
502
maruel@chromium.org5c3a2ff2009-05-12 19:28:55 +0000503def CaptureSVN(args, in_directory=None, print_error=True):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000504 """Runs svn, capturing output sent to stdout as a string.
505
506 Args:
507 args: A sequence of command line parameters to be passed to svn.
508 in_directory: The directory where svn is to be run.
509
510 Returns:
511 The output sent to stdout as a string.
512 """
513 c = [SVN_COMMAND]
514 c.extend(args)
515
516 # *Sigh*: Windows needs shell=True, or else it won't search %PATH% for
517 # the svn.exe executable, but shell=True makes subprocess on Linux fail
518 # when it's called with a list because it only tries to execute the
519 # first string ("svn").
maruel@chromium.org5c3a2ff2009-05-12 19:28:55 +0000520 stderr = None
maruel@chromium.org672343d2009-05-20 20:03:25 +0000521 if not print_error:
maruel@chromium.org5c3a2ff2009-05-12 19:28:55 +0000522 stderr = subprocess.PIPE
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000523 return subprocess.Popen(c,
524 cwd=in_directory,
525 shell=(sys.platform == 'win32'),
maruel@chromium.org5c3a2ff2009-05-12 19:28:55 +0000526 stdout=subprocess.PIPE,
527 stderr=stderr).communicate()[0]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000528
529
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000530def RunSVNAndGetFileList(args, in_directory, file_list):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000531 """Runs svn checkout, update, or status, output to stdout.
532
533 The first item in args must be either "checkout", "update", or "status".
534
535 svn's stdout is parsed to collect a list of files checked out or updated.
536 These files are appended to file_list. svn's stdout is also printed to
537 sys.stdout as in RunSVN.
538
539 Args:
540 args: A sequence of command line parameters to be passed to svn.
541 in_directory: The directory where svn is to be run.
542
543 Raises:
544 Error: An error occurred while running the svn command.
545 """
546 command = [SVN_COMMAND]
547 command.extend(args)
548
549 # svn update and svn checkout use the same pattern: the first three columns
550 # are for file status, property status, and lock status. This is followed
551 # by two spaces, and then the path to the file.
552 update_pattern = '^... (.*)$'
553
554 # The first three columns of svn status are the same as for svn update and
555 # svn checkout. The next three columns indicate addition-with-history,
556 # switch, and remote lock status. This is followed by one space, and then
557 # the path to the file.
558 status_pattern = '^...... (.*)$'
559
560 # args[0] must be a supported command. This will blow up if it's something
561 # else, which is good. Note that the patterns are only effective when
562 # these commands are used in their ordinary forms, the patterns are invalid
563 # for "svn status --show-updates", for example.
564 pattern = {
565 'checkout': update_pattern,
566 'status': status_pattern,
567 'update': update_pattern,
568 }[args[0]]
569
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000570 SubprocessCallAndCapture(command,
571 in_directory,
572 pattern=pattern,
573 capture_list=file_list)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000574
575
maruel@chromium.org5c3a2ff2009-05-12 19:28:55 +0000576def CaptureSVNInfo(relpath, in_directory=None, print_error=True):
maruel@chromium.org2dc8a4d2009-05-11 12:41:20 +0000577 """Returns a dictionary from the svn info output for the given file.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000578
579 Args:
580 relpath: The directory where the working copy resides relative to
581 the directory given by in_directory.
582 in_directory: The directory where svn is to be run.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000583 """
maruel@chromium.org5c3a2ff2009-05-12 19:28:55 +0000584 output = CaptureSVN(["info", "--xml", relpath], in_directory, print_error)
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000585 dom = ParseXML(output)
maruel@chromium.org2dc8a4d2009-05-11 12:41:20 +0000586 result = {}
maruel@chromium.org483b0082009-05-07 02:57:14 +0000587 if dom:
maruel@chromium.org2dc8a4d2009-05-11 12:41:20 +0000588 def C(item, f):
589 if item is not None: return f(item)
maruel@chromium.org483b0082009-05-07 02:57:14 +0000590 # /info/entry/
591 # url
592 # reposityory/(root|uuid)
593 # wc-info/(schedule|depth)
594 # commit/(author|date)
595 # str() the results because they may be returned as Unicode, which
596 # interferes with the higher layers matching up things in the deps
597 # dictionary.
maruel@chromium.org2dc8a4d2009-05-11 12:41:20 +0000598 # TODO(maruel): Fix at higher level instead (!)
599 result['Repository Root'] = C(GetNamedNodeText(dom, 'root'), str)
600 result['URL'] = C(GetNamedNodeText(dom, 'url'), str)
601 result['UUID'] = C(GetNamedNodeText(dom, 'uuid'), str)
602 result['Revision'] = C(GetNodeNamedAttributeText(dom, 'entry', 'revision'),
603 int)
604 result['Node Kind'] = C(GetNodeNamedAttributeText(dom, 'entry', 'kind'),
605 str)
606 result['Schedule'] = C(GetNamedNodeText(dom, 'schedule'), str)
607 result['Path'] = C(GetNodeNamedAttributeText(dom, 'entry', 'path'), str)
608 result['Copied From URL'] = C(GetNamedNodeText(dom, 'copy-from-url'), str)
609 result['Copied From Rev'] = C(GetNamedNodeText(dom, 'copy-from-rev'), str)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000610 return result
611
612
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000613def CaptureSVNHeadRevision(url):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000614 """Get the head revision of a SVN repository.
615
616 Returns:
617 Int head revision
618 """
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000619 info = CaptureSVN(["info", "--xml", url], os.getcwd())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000620 dom = xml.dom.minidom.parseString(info)
621 return int(dom.getElementsByTagName('entry')[0].getAttribute('revision'))
622
623
maruel@chromium.org4810a962009-05-12 21:03:34 +0000624def CaptureSVNStatus(files):
625 """Returns the svn 1.5 svn status emulated output.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000626
maruel@chromium.org4810a962009-05-12 21:03:34 +0000627 @files can be a string (one file) or a list of files.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000628
maruel@chromium.org4810a962009-05-12 21:03:34 +0000629 Returns an array of (status, file) tuples."""
630 command = ["status", "--xml"]
631 if not files:
632 pass
633 elif isinstance(files, basestring):
634 command.append(files)
635 else:
636 command.extend(files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000637
maruel@chromium.org4810a962009-05-12 21:03:34 +0000638 status_letter = {
639 None: ' ',
640 '': ' ',
641 'added': 'A',
642 'conflicted': 'C',
643 'deleted': 'D',
644 'external': 'X',
645 'ignored': 'I',
646 'incomplete': '!',
647 'merged': 'G',
648 'missing': '!',
649 'modified': 'M',
650 'none': ' ',
651 'normal': ' ',
652 'obstructed': '~',
653 'replaced': 'R',
654 'unversioned': '?',
655 }
656 dom = ParseXML(CaptureSVN(command))
maruel@chromium.orge105d8d2009-04-30 17:58:25 +0000657 results = []
658 if dom:
659 # /status/target/entry/(wc-status|commit|author|date)
660 for target in dom.getElementsByTagName('target'):
661 base_path = target.getAttribute('path')
662 for entry in target.getElementsByTagName('entry'):
663 file = entry.getAttribute('path')
664 wc_status = entry.getElementsByTagName('wc-status')
665 assert len(wc_status) == 1
666 # Emulate svn 1.5 status ouput...
667 statuses = [' ' for i in range(7)]
668 # Col 0
669 xml_item_status = wc_status[0].getAttribute('item')
maruel@chromium.org4810a962009-05-12 21:03:34 +0000670 if xml_item_status in status_letter:
671 statuses[0] = status_letter[xml_item_status]
maruel@chromium.orge105d8d2009-04-30 17:58:25 +0000672 else:
673 raise Exception('Unknown item status "%s"; please implement me!' %
674 xml_item_status)
675 # Col 1
676 xml_props_status = wc_status[0].getAttribute('props')
677 if xml_props_status == 'modified':
678 statuses[1] = 'M'
679 elif xml_props_status == 'conflicted':
680 statuses[1] = 'C'
681 elif (not xml_props_status or xml_props_status == 'none' or
682 xml_props_status == 'normal'):
683 pass
684 else:
685 raise Exception('Unknown props status "%s"; please implement me!' %
686 xml_props_status)
maruel@chromium.orgedd27d12009-05-01 17:46:56 +0000687 # Col 2
688 if wc_status[0].getAttribute('wc-locked') == 'true':
689 statuses[2] = 'L'
maruel@chromium.orge105d8d2009-04-30 17:58:25 +0000690 # Col 3
691 if wc_status[0].getAttribute('copied') == 'true':
692 statuses[3] = '+'
maruel@chromium.org4810a962009-05-12 21:03:34 +0000693 item = (''.join(statuses), file)
maruel@chromium.orge105d8d2009-04-30 17:58:25 +0000694 results.append(item)
695 return results
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000696
697
698### SCM abstraction layer
699
700
701class SCMWrapper(object):
702 """Add necessary glue between all the supported SCM.
703
704 This is the abstraction layer to bind to different SCM. Since currently only
705 subversion is supported, a lot of subersionism remains. This can be sorted out
706 once another SCM is supported."""
707 def __init__(self, url=None, root_dir=None, relpath=None,
708 scm_name='svn'):
709 # TODO(maruel): Deduce the SCM from the url.
710 self.scm_name = scm_name
711 self.url = url
712 self._root_dir = root_dir
713 if self._root_dir:
maruel@chromium.orge105d8d2009-04-30 17:58:25 +0000714 self._root_dir = self._root_dir.replace('/', os.sep)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000715 self.relpath = relpath
716 if self.relpath:
maruel@chromium.orge105d8d2009-04-30 17:58:25 +0000717 self.relpath = self.relpath.replace('/', os.sep)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000718
719 def FullUrlForRelativeUrl(self, url):
720 # Find the forth '/' and strip from there. A bit hackish.
721 return '/'.join(self.url.split('/')[:4]) + url
722
723 def RunCommand(self, command, options, args, file_list=None):
724 # file_list will have all files that are modified appended to it.
725
726 if file_list == None:
727 file_list = []
728
729 commands = {
730 'cleanup': self.cleanup,
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +0000731 'export': self.export,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000732 'update': self.update,
733 'revert': self.revert,
734 'status': self.status,
735 'diff': self.diff,
736 'runhooks': self.status,
737 }
738
739 if not command in commands:
740 raise Error('Unknown command %s' % command)
741
742 return commands[command](options, args, file_list)
743
744 def cleanup(self, options, args, file_list):
745 """Cleanup working copy."""
746 command = ['cleanup']
747 command.extend(args)
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000748 RunSVN(command, os.path.join(self._root_dir, self.relpath))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000749
750 def diff(self, options, args, file_list):
751 # NOTE: This function does not currently modify file_list.
752 command = ['diff']
753 command.extend(args)
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000754 RunSVN(command, os.path.join(self._root_dir, self.relpath))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000755
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +0000756 def export(self, options, args, file_list):
757 assert len(args) == 1
758 export_path = os.path.abspath(os.path.join(args[0], self.relpath))
759 try:
760 os.makedirs(export_path)
761 except OSError:
762 pass
763 assert os.path.exists(export_path)
764 command = ['export', '--force', '.']
765 command.append(export_path)
766 RunSVN(command, os.path.join(self._root_dir, self.relpath))
767
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000768 def update(self, options, args, file_list):
769 """Runs SCM to update or transparently checkout the working copy.
770
771 All updated files will be appended to file_list.
772
773 Raises:
774 Error: if can't get URL for relative path.
775 """
776 # Only update if git is not controlling the directory.
maruel@chromium.org8626ff72009-05-13 02:57:02 +0000777 checkout_path = os.path.join(self._root_dir, self.relpath)
maruel@chromium.org0329e672009-05-13 18:41:04 +0000778 git_path = os.path.join(self._root_dir, self.relpath, '.git')
779 if os.path.exists(git_path):
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000780 print("________ found .git directory; skipping %s" % self.relpath)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000781 return
782
783 if args:
784 raise Error("Unsupported argument(s): %s" % ",".join(args))
785
786 url = self.url
787 components = url.split("@")
788 revision = None
789 forced_revision = False
790 if options.revision:
791 # Override the revision number.
792 url = '%s@%s' % (components[0], str(options.revision))
793 revision = int(options.revision)
794 forced_revision = True
795 elif len(components) == 2:
796 revision = int(components[1])
797 forced_revision = True
798
799 rev_str = ""
800 if revision:
801 rev_str = ' at %d' % revision
802
maruel@chromium.org0329e672009-05-13 18:41:04 +0000803 if not os.path.exists(checkout_path):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000804 # We need to checkout.
maruel@chromium.org8626ff72009-05-13 02:57:02 +0000805 command = ['checkout', url, checkout_path]
maruel@chromium.orgedd27d12009-05-01 17:46:56 +0000806 if revision:
807 command.extend(['--revision', str(revision)])
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000808 RunSVNAndGetFileList(command, self._root_dir, file_list)
maruel@chromium.orgedd27d12009-05-01 17:46:56 +0000809 return
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000810
811 # Get the existing scm url and the revision number of the current checkout.
maruel@chromium.org8626ff72009-05-13 02:57:02 +0000812 from_info = CaptureSVNInfo(os.path.join(checkout_path, '.'), '.')
maruel@chromium.org1998c6d2009-05-15 12:38:12 +0000813 if not from_info:
814 raise Error("Can't update/checkout %r if an unversioned directory is "
815 "present. Delete the directory and try again." %
816 checkout_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000817
818 if options.manually_grab_svn_rev:
819 # Retrieve the current HEAD version because svn is slow at null updates.
820 if not revision:
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000821 from_info_live = CaptureSVNInfo(from_info['URL'], '.')
maruel@chromium.org2dc8a4d2009-05-11 12:41:20 +0000822 revision = int(from_info_live['Revision'])
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000823 rev_str = ' at %d' % revision
824
maruel@chromium.org2dc8a4d2009-05-11 12:41:20 +0000825 if from_info['URL'] != components[0]:
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000826 to_info = CaptureSVNInfo(url, '.')
maruel@chromium.org8626ff72009-05-13 02:57:02 +0000827 can_switch = ((from_info['Repository Root'] != to_info['Repository Root'])
828 and (from_info['UUID'] == to_info['UUID']))
829 if can_switch:
830 print("\n_____ relocating %s to a new checkout" % self.relpath)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000831 # We have different roots, so check if we can switch --relocate.
832 # Subversion only permits this if the repository UUIDs match.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000833 # Perform the switch --relocate, then rewrite the from_url
834 # to reflect where we "are now." (This is the same way that
835 # Subversion itself handles the metadata when switch --relocate
836 # is used.) This makes the checks below for whether we
837 # can update to a revision or have to switch to a different
838 # branch work as expected.
839 # TODO(maruel): TEST ME !
maruel@chromium.org2dc8a4d2009-05-11 12:41:20 +0000840 command = ["switch", "--relocate",
841 from_info['Repository Root'],
842 to_info['Repository Root'],
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000843 self.relpath]
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000844 RunSVN(command, self._root_dir)
maruel@chromium.org2dc8a4d2009-05-11 12:41:20 +0000845 from_info['URL'] = from_info['URL'].replace(
846 from_info['Repository Root'],
847 to_info['Repository Root'])
maruel@chromium.org8626ff72009-05-13 02:57:02 +0000848 else:
849 if CaptureSVNStatus(checkout_path):
850 raise Error("Can't switch the checkout to %s; UUID don't match and "
851 "there is local changes in %s. Delete the directory and "
852 "try again." % (url, checkout_path))
853 # Ok delete it.
854 print("\n_____ switching %s to a new checkout" % self.relpath)
855 RemoveDirectory(checkout_path)
856 # We need to checkout.
857 command = ['checkout', url, checkout_path]
858 if revision:
859 command.extend(['--revision', str(revision)])
860 RunSVNAndGetFileList(command, self._root_dir, file_list)
861 return
862
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000863
864 # If the provided url has a revision number that matches the revision
865 # number of the existing directory, then we don't need to bother updating.
maruel@chromium.org2dc8a4d2009-05-11 12:41:20 +0000866 if not options.force and from_info['Revision'] == revision:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000867 if options.verbose or not forced_revision:
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000868 print("\n_____ %s%s" % (self.relpath, rev_str))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000869 return
870
maruel@chromium.org8626ff72009-05-13 02:57:02 +0000871 command = ["update", checkout_path]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000872 if revision:
873 command.extend(['--revision', str(revision)])
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000874 RunSVNAndGetFileList(command, self._root_dir, file_list)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000875
876 def revert(self, options, args, file_list):
877 """Reverts local modifications. Subversion specific.
878
879 All reverted files will be appended to file_list, even if Subversion
880 doesn't know about them.
881 """
882 path = os.path.join(self._root_dir, self.relpath)
883 if not os.path.isdir(path):
maruel@chromium.orgedd27d12009-05-01 17:46:56 +0000884 # svn revert won't work if the directory doesn't exist. It needs to
885 # checkout instead.
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000886 print("\n_____ %s is missing, synching instead" % self.relpath)
maruel@chromium.orgedd27d12009-05-01 17:46:56 +0000887 # Don't reuse the args.
888 return self.update(options, [], file_list)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000889
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000890 files = CaptureSVNStatus(path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000891 # Batch the command.
892 files_to_revert = []
893 for file in files:
maruel@chromium.org4810a962009-05-12 21:03:34 +0000894 file_path = os.path.join(path, file[1])
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000895 print(file_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000896 # Unversioned file or unexpected unversioned file.
maruel@chromium.org4810a962009-05-12 21:03:34 +0000897 if file[0][0] in ('?', '~'):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000898 # Remove extraneous file. Also remove unexpected unversioned
899 # directories. svn won't touch them but we want to delete these.
900 file_list.append(file_path)
901 try:
902 os.remove(file_path)
903 except EnvironmentError:
904 RemoveDirectory(file_path)
905
maruel@chromium.org4810a962009-05-12 21:03:34 +0000906 if file[0][0] != '?':
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000907 # For any other status, svn revert will work.
908 file_list.append(file_path)
maruel@chromium.org4810a962009-05-12 21:03:34 +0000909 files_to_revert.append(file[1])
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000910
911 # Revert them all at once.
912 if files_to_revert:
913 accumulated_paths = []
914 accumulated_length = 0
915 command = ['revert']
916 for p in files_to_revert:
917 # Some shell have issues with command lines too long.
918 if accumulated_length and accumulated_length + len(p) > 3072:
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000919 RunSVN(command + accumulated_paths,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000920 os.path.join(self._root_dir, self.relpath))
921 accumulated_paths = []
922 accumulated_length = 0
923 else:
924 accumulated_paths.append(p)
925 accumulated_length += len(p)
926 if accumulated_paths:
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000927 RunSVN(command + accumulated_paths,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000928 os.path.join(self._root_dir, self.relpath))
929
930 def status(self, options, args, file_list):
931 """Display status information."""
maruel@chromium.orgedd27d12009-05-01 17:46:56 +0000932 path = os.path.join(self._root_dir, self.relpath)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000933 command = ['status']
934 command.extend(args)
maruel@chromium.orgedd27d12009-05-01 17:46:56 +0000935 if not os.path.isdir(path):
936 # svn status won't work if the directory doesn't exist.
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000937 print("\n________ couldn't run \'%s\' in \'%s\':\nThe directory "
938 "does not exist."
939 % (' '.join(command), path))
maruel@chromium.orgedd27d12009-05-01 17:46:56 +0000940 # There's no file list to retrieve.
941 else:
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000942 RunSVNAndGetFileList(command, path, file_list)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000943
944
945## GClient implementation.
946
947
948class GClient(object):
949 """Object that represent a gclient checkout."""
950
951 supported_commands = [
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +0000952 'cleanup', 'diff', 'export', 'revert', 'status', 'update', 'runhooks'
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000953 ]
954
955 def __init__(self, root_dir, options):
956 self._root_dir = root_dir
957 self._options = options
958 self._config_content = None
959 self._config_dict = {}
960 self._deps_hooks = []
961
962 def SetConfig(self, content):
963 self._config_dict = {}
964 self._config_content = content
skylined@chromium.orgdf0032c2009-05-29 10:43:56 +0000965 try:
966 exec(content, self._config_dict)
967 except SyntaxError, e:
968 try:
969 # Try to construct a human readable error message
970 error_message = [
971 'There is a syntax error in your configuration file.',
972 'Line #%s, character %s:' % (e.lineno, e.offset),
973 '"%s"' % re.sub(r'[\r\n]*$', '', e.text) ]
974 except:
975 # Something went wrong, re-raise the original exception
976 raise e
977 else:
978 # Raise a new exception with the human readable message:
979 raise Error('\n'.join(error_message))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000980
981 def SaveConfig(self):
982 FileWrite(os.path.join(self._root_dir, self._options.config_filename),
983 self._config_content)
984
985 def _LoadConfig(self):
986 client_source = FileRead(os.path.join(self._root_dir,
987 self._options.config_filename))
988 self.SetConfig(client_source)
989
990 def ConfigContent(self):
991 return self._config_content
992
993 def GetVar(self, key, default=None):
994 return self._config_dict.get(key, default)
995
996 @staticmethod
997 def LoadCurrentConfig(options, from_dir=None):
998 """Searches for and loads a .gclient file relative to the current working
999 dir.
1000
1001 Returns:
1002 A dict representing the contents of the .gclient file or an empty dict if
1003 the .gclient file doesn't exist.
1004 """
1005 if not from_dir:
1006 from_dir = os.curdir
1007 path = os.path.realpath(from_dir)
maruel@chromium.org0329e672009-05-13 18:41:04 +00001008 while not os.path.exists(os.path.join(path, options.config_filename)):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001009 next = os.path.split(path)
1010 if not next[1]:
1011 return None
1012 path = next[0]
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001013 client = GClient(path, options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001014 client._LoadConfig()
1015 return client
1016
1017 def SetDefaultConfig(self, solution_name, solution_url, safesync_url):
1018 self.SetConfig(DEFAULT_CLIENT_FILE_TEXT % (
1019 solution_name, solution_url, safesync_url
1020 ))
1021
1022 def _SaveEntries(self, entries):
1023 """Creates a .gclient_entries file to record the list of unique checkouts.
1024
1025 The .gclient_entries file lives in the same directory as .gclient.
1026
1027 Args:
1028 entries: A sequence of solution names.
1029 """
1030 text = "entries = [\n"
1031 for entry in entries:
1032 text += " \"%s\",\n" % entry
1033 text += "]\n"
1034 FileWrite(os.path.join(self._root_dir, self._options.entries_filename),
1035 text)
1036
1037 def _ReadEntries(self):
1038 """Read the .gclient_entries file for the given client.
1039
1040 Args:
1041 client: The client for which the entries file should be read.
1042
1043 Returns:
1044 A sequence of solution names, which will be empty if there is the
1045 entries file hasn't been created yet.
1046 """
1047 scope = {}
1048 filename = os.path.join(self._root_dir, self._options.entries_filename)
maruel@chromium.org0329e672009-05-13 18:41:04 +00001049 if not os.path.exists(filename):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001050 return []
1051 exec(FileRead(filename), scope)
1052 return scope["entries"]
1053
1054 class FromImpl:
1055 """Used to implement the From syntax."""
1056
1057 def __init__(self, module_name):
1058 self.module_name = module_name
1059
1060 def __str__(self):
1061 return 'From("%s")' % self.module_name
1062
1063 class _VarImpl:
1064 def __init__(self, custom_vars, local_scope):
1065 self._custom_vars = custom_vars
1066 self._local_scope = local_scope
1067
1068 def Lookup(self, var_name):
1069 """Implements the Var syntax."""
1070 if var_name in self._custom_vars:
1071 return self._custom_vars[var_name]
1072 elif var_name in self._local_scope.get("vars", {}):
1073 return self._local_scope["vars"][var_name]
1074 raise Error("Var is not defined: %s" % var_name)
1075
1076 def _ParseSolutionDeps(self, solution_name, solution_deps_content,
1077 custom_vars):
1078 """Parses the DEPS file for the specified solution.
1079
1080 Args:
1081 solution_name: The name of the solution to query.
1082 solution_deps_content: Content of the DEPS file for the solution
1083 custom_vars: A dict of vars to override any vars defined in the DEPS file.
1084
1085 Returns:
1086 A dict mapping module names (as relative paths) to URLs or an empty
1087 dict if the solution does not have a DEPS file.
1088 """
1089 # Skip empty
1090 if not solution_deps_content:
1091 return {}
1092 # Eval the content
1093 local_scope = {}
1094 var = self._VarImpl(custom_vars, local_scope)
1095 global_scope = {"From": self.FromImpl, "Var": var.Lookup, "deps_os": {}}
1096 exec(solution_deps_content, global_scope, local_scope)
1097 deps = local_scope.get("deps", {})
1098
1099 # load os specific dependencies if defined. these dependencies may
1100 # override or extend the values defined by the 'deps' member.
1101 if "deps_os" in local_scope:
1102 deps_os_choices = {
1103 "win32": "win",
1104 "win": "win",
1105 "cygwin": "win",
1106 "darwin": "mac",
1107 "mac": "mac",
1108 "unix": "unix",
1109 "linux": "unix",
1110 "linux2": "unix",
1111 }
1112
1113 if self._options.deps_os is not None:
1114 deps_to_include = self._options.deps_os.split(",")
1115 if "all" in deps_to_include:
1116 deps_to_include = deps_os_choices.values()
1117 else:
1118 deps_to_include = [deps_os_choices.get(self._options.platform, "unix")]
1119
1120 deps_to_include = set(deps_to_include)
1121 for deps_os_key in deps_to_include:
1122 os_deps = local_scope["deps_os"].get(deps_os_key, {})
1123 if len(deps_to_include) > 1:
1124 # Ignore any overrides when including deps for more than one
1125 # platform, so we collect the broadest set of dependencies available.
1126 # We may end up with the wrong revision of something for our
1127 # platform, but this is the best we can do.
1128 deps.update([x for x in os_deps.items() if not x[0] in deps])
1129 else:
1130 deps.update(os_deps)
1131
1132 if 'hooks' in local_scope:
1133 self._deps_hooks.extend(local_scope['hooks'])
1134
1135 # If use_relative_paths is set in the DEPS file, regenerate
1136 # the dictionary using paths relative to the directory containing
1137 # the DEPS file.
1138 if local_scope.get('use_relative_paths'):
1139 rel_deps = {}
1140 for d, url in deps.items():
1141 # normpath is required to allow DEPS to use .. in their
1142 # dependency local path.
1143 rel_deps[os.path.normpath(os.path.join(solution_name, d))] = url
1144 return rel_deps
1145 else:
1146 return deps
1147
1148 def _ParseAllDeps(self, solution_urls, solution_deps_content):
1149 """Parse the complete list of dependencies for the client.
1150
1151 Args:
1152 solution_urls: A dict mapping module names (as relative paths) to URLs
1153 corresponding to the solutions specified by the client. This parameter
1154 is passed as an optimization.
1155 solution_deps_content: A dict mapping module names to the content
1156 of their DEPS files
1157
1158 Returns:
1159 A dict mapping module names (as relative paths) to URLs corresponding
1160 to the entire set of dependencies to checkout for the given client.
1161
1162 Raises:
1163 Error: If a dependency conflicts with another dependency or of a solution.
1164 """
1165 deps = {}
1166 for solution in self.GetVar("solutions"):
1167 custom_vars = solution.get("custom_vars", {})
1168 solution_deps = self._ParseSolutionDeps(
1169 solution["name"],
1170 solution_deps_content[solution["name"]],
1171 custom_vars)
1172
1173 # If a line is in custom_deps, but not in the solution, we want to append
1174 # this line to the solution.
1175 if "custom_deps" in solution:
1176 for d in solution["custom_deps"]:
1177 if d not in solution_deps:
1178 solution_deps[d] = solution["custom_deps"][d]
1179
1180 for d in solution_deps:
1181 if "custom_deps" in solution and d in solution["custom_deps"]:
1182 # Dependency is overriden.
1183 url = solution["custom_deps"][d]
1184 if url is None:
1185 continue
1186 else:
1187 url = solution_deps[d]
1188 # if we have a From reference dependent on another solution, then
1189 # just skip the From reference. When we pull deps for the solution,
1190 # we will take care of this dependency.
1191 #
1192 # If multiple solutions all have the same From reference, then we
1193 # should only add one to our list of dependencies.
1194 if type(url) != str:
1195 if url.module_name in solution_urls:
1196 # Already parsed.
1197 continue
1198 if d in deps and type(deps[d]) != str:
1199 if url.module_name == deps[d].module_name:
1200 continue
1201 else:
1202 parsed_url = urlparse.urlparse(url)
1203 scheme = parsed_url[0]
1204 if not scheme:
1205 # A relative url. Fetch the real base.
1206 path = parsed_url[2]
1207 if path[0] != "/":
1208 raise Error(
1209 "relative DEPS entry \"%s\" must begin with a slash" % d)
1210 # Create a scm just to query the full url.
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001211 scm = SCMWrapper(solution["url"], self._root_dir, None)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001212 url = scm.FullUrlForRelativeUrl(url)
1213 if d in deps and deps[d] != url:
1214 raise Error(
1215 "Solutions have conflicting versions of dependency \"%s\"" % d)
1216 if d in solution_urls and solution_urls[d] != url:
1217 raise Error(
1218 "Dependency \"%s\" conflicts with specified solution" % d)
1219 # Grab the dependency.
1220 deps[d] = url
1221 return deps
1222
1223 def _RunHookAction(self, hook_dict):
1224 """Runs the action from a single hook.
1225 """
1226 command = hook_dict['action'][:]
1227 if command[0] == 'python':
1228 # If the hook specified "python" as the first item, the action is a
1229 # Python script. Run it by starting a new copy of the same
1230 # interpreter.
1231 command[0] = sys.executable
1232
1233 # Use a discrete exit status code of 2 to indicate that a hook action
1234 # failed. Users of this script may wish to treat hook action failures
1235 # differently from VC failures.
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +00001236 SubprocessCall(command, self._root_dir, fail_status=2)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001237
1238 def _RunHooks(self, command, file_list, is_using_git):
1239 """Evaluates all hooks, running actions as needed.
1240 """
1241 # Hooks only run for these command types.
1242 if not command in ('update', 'revert', 'runhooks'):
1243 return
1244
evan@chromium.org67820ef2009-07-27 17:23:00 +00001245 # Hooks only run when --nohooks is not specified
1246 if self._options.nohooks:
1247 return
1248
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001249 # Get any hooks from the .gclient file.
1250 hooks = self.GetVar("hooks", [])
1251 # Add any hooks found in DEPS files.
1252 hooks.extend(self._deps_hooks)
1253
1254 # If "--force" was specified, run all hooks regardless of what files have
1255 # changed. If the user is using git, then we don't know what files have
1256 # changed so we always run all hooks.
1257 if self._options.force or is_using_git:
1258 for hook_dict in hooks:
1259 self._RunHookAction(hook_dict)
1260 return
1261
1262 # Run hooks on the basis of whether the files from the gclient operation
1263 # match each hook's pattern.
1264 for hook_dict in hooks:
1265 pattern = re.compile(hook_dict['pattern'])
1266 for file in file_list:
1267 if not pattern.search(file):
1268 continue
1269
1270 self._RunHookAction(hook_dict)
1271
1272 # The hook's action only runs once. Don't bother looking for any
1273 # more matches.
1274 break
1275
1276 def RunOnDeps(self, command, args):
1277 """Runs a command on each dependency in a client and its dependencies.
1278
1279 The module's dependencies are specified in its top-level DEPS files.
1280
1281 Args:
1282 command: The command to use (e.g., 'status' or 'diff')
1283 args: list of str - extra arguments to add to the command line.
1284
1285 Raises:
1286 Error: If the client has conflicting entries.
1287 """
1288 if not command in self.supported_commands:
1289 raise Error("'%s' is an unsupported command" % command)
1290
1291 # Check for revision overrides.
1292 revision_overrides = {}
1293 for revision in self._options.revisions:
1294 if revision.find("@") == -1:
1295 raise Error(
1296 "Specify the full dependency when specifying a revision number.")
1297 revision_elem = revision.split("@")
1298 # Disallow conflicting revs
1299 if revision_overrides.has_key(revision_elem[0]) and \
1300 revision_overrides[revision_elem[0]] != revision_elem[1]:
1301 raise Error(
1302 "Conflicting revision numbers specified.")
1303 revision_overrides[revision_elem[0]] = revision_elem[1]
1304
1305 solutions = self.GetVar("solutions")
1306 if not solutions:
1307 raise Error("No solution specified")
1308
1309 # When running runhooks --force, there's no need to consult the SCM.
1310 # All known hooks are expected to run unconditionally regardless of working
1311 # copy state, so skip the SCM status check.
1312 run_scm = not (command == 'runhooks' and self._options.force)
1313
1314 entries = {}
1315 entries_deps_content = {}
1316 file_list = []
1317 # Run on the base solutions first.
1318 for solution in solutions:
1319 name = solution["name"]
1320 if name in entries:
1321 raise Error("solution %s specified more than once" % name)
1322 url = solution["url"]
1323 entries[name] = url
1324 if run_scm:
1325 self._options.revision = revision_overrides.get(name)
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001326 scm = SCMWrapper(url, self._root_dir, name)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001327 scm.RunCommand(command, self._options, args, file_list)
1328 self._options.revision = None
1329 try:
1330 deps_content = FileRead(os.path.join(self._root_dir, name,
1331 self._options.deps_file))
1332 except IOError, e:
1333 if e.errno != errno.ENOENT:
1334 raise
1335 deps_content = ""
1336 entries_deps_content[name] = deps_content
1337
1338 # Process the dependencies next (sort alphanumerically to ensure that
1339 # containing directories get populated first and for readability)
1340 deps = self._ParseAllDeps(entries, entries_deps_content)
1341 deps_to_process = deps.keys()
1342 deps_to_process.sort()
1343
1344 # First pass for direct dependencies.
1345 for d in deps_to_process:
1346 if type(deps[d]) == str:
1347 url = deps[d]
1348 entries[d] = url
1349 if run_scm:
1350 self._options.revision = revision_overrides.get(d)
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001351 scm = SCMWrapper(url, self._root_dir, d)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001352 scm.RunCommand(command, self._options, args, file_list)
1353 self._options.revision = None
1354
1355 # Second pass for inherited deps (via the From keyword)
1356 for d in deps_to_process:
1357 if type(deps[d]) != str:
1358 sub_deps = self._ParseSolutionDeps(
1359 deps[d].module_name,
1360 FileRead(os.path.join(self._root_dir,
1361 deps[d].module_name,
1362 self._options.deps_file)),
1363 {})
1364 url = sub_deps[d]
1365 entries[d] = url
1366 if run_scm:
1367 self._options.revision = revision_overrides.get(d)
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001368 scm = SCMWrapper(url, self._root_dir, d)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001369 scm.RunCommand(command, self._options, args, file_list)
1370 self._options.revision = None
1371
1372 is_using_git = IsUsingGit(self._root_dir, entries.keys())
1373 self._RunHooks(command, file_list, is_using_git)
1374
1375 if command == 'update':
ajwong@chromium.orgcdcee802009-06-23 15:30:42 +00001376 # Notify the user if there is an orphaned entry in their working copy.
1377 # Only delete the directory if there are no changes in it, and
1378 # delete_unversioned_trees is set to true.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001379 prev_entries = self._ReadEntries()
1380 for entry in prev_entries:
1381 e_dir = os.path.join(self._root_dir, entry)
maruel@chromium.org0329e672009-05-13 18:41:04 +00001382 if entry not in entries and os.path.exists(e_dir):
ajwong@chromium.org8399dc02009-06-23 21:36:25 +00001383 if not self._options.delete_unversioned_trees or \
1384 CaptureSVNStatus(e_dir):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001385 # There are modified files in this entry
1386 entries[entry] = None # Keep warning until removed.
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +00001387 print("\nWARNING: \"%s\" is no longer part of this client. "
1388 "It is recommended that you manually remove it.\n") % entry
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001389 else:
1390 # Delete the entry
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +00001391 print("\n________ deleting \'%s\' " +
1392 "in \'%s\'") % (entry, self._root_dir)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001393 RemoveDirectory(e_dir)
1394 # record the current list of entries for next time
1395 self._SaveEntries(entries)
1396
1397 def PrintRevInfo(self):
1398 """Output revision info mapping for the client and its dependencies. This
1399 allows the capture of a overall "revision" for the source tree that can
1400 be used to reproduce the same tree in the future. The actual output
1401 contains enough information (source paths, svn server urls and revisions)
1402 that it can be used either to generate external svn commands (without
1403 gclient) or as input to gclient's --rev option (with some massaging of
1404 the data).
1405
1406 NOTE: Unlike RunOnDeps this does not require a local checkout and is run
1407 on the Pulse master. It MUST NOT execute hooks.
1408
1409 Raises:
1410 Error: If the client has conflicting entries.
1411 """
1412 # Check for revision overrides.
1413 revision_overrides = {}
1414 for revision in self._options.revisions:
1415 if revision.find("@") < 0:
1416 raise Error(
1417 "Specify the full dependency when specifying a revision number.")
1418 revision_elem = revision.split("@")
1419 # Disallow conflicting revs
1420 if revision_overrides.has_key(revision_elem[0]) and \
1421 revision_overrides[revision_elem[0]] != revision_elem[1]:
1422 raise Error(
1423 "Conflicting revision numbers specified.")
1424 revision_overrides[revision_elem[0]] = revision_elem[1]
1425
1426 solutions = self.GetVar("solutions")
1427 if not solutions:
1428 raise Error("No solution specified")
1429
1430 entries = {}
1431 entries_deps_content = {}
1432
1433 # Inner helper to generate base url and rev tuple (including honoring
1434 # |revision_overrides|)
1435 def GetURLAndRev(name, original_url):
1436 if original_url.find("@") < 0:
1437 if revision_overrides.has_key(name):
1438 return (original_url, int(revision_overrides[name]))
1439 else:
1440 # TODO(aharper): SVN/SCMWrapper cleanup (non-local commandset)
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +00001441 return (original_url, CaptureSVNHeadRevision(original_url))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001442 else:
1443 url_components = original_url.split("@")
1444 if revision_overrides.has_key(name):
1445 return (url_components[0], int(revision_overrides[name]))
1446 else:
1447 return (url_components[0], int(url_components[1]))
1448
1449 # Run on the base solutions first.
1450 for solution in solutions:
1451 name = solution["name"]
1452 if name in entries:
1453 raise Error("solution %s specified more than once" % name)
1454 (url, rev) = GetURLAndRev(name, solution["url"])
1455 entries[name] = "%s@%d" % (url, rev)
1456 # TODO(aharper): SVN/SCMWrapper cleanup (non-local commandset)
1457 entries_deps_content[name] = CaptureSVN(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001458 ["cat",
1459 "%s/%s@%d" % (url,
1460 self._options.deps_file,
1461 rev)],
1462 os.getcwd())
1463
1464 # Process the dependencies next (sort alphanumerically to ensure that
1465 # containing directories get populated first and for readability)
1466 deps = self._ParseAllDeps(entries, entries_deps_content)
1467 deps_to_process = deps.keys()
1468 deps_to_process.sort()
1469
1470 # First pass for direct dependencies.
1471 for d in deps_to_process:
1472 if type(deps[d]) == str:
1473 (url, rev) = GetURLAndRev(d, deps[d])
1474 entries[d] = "%s@%d" % (url, rev)
1475
1476 # Second pass for inherited deps (via the From keyword)
1477 for d in deps_to_process:
1478 if type(deps[d]) != str:
1479 deps_parent_url = entries[deps[d].module_name]
1480 if deps_parent_url.find("@") < 0:
1481 raise Error("From %s missing revisioned url" % deps[d].module_name)
1482 deps_parent_url_components = deps_parent_url.split("@")
1483 # TODO(aharper): SVN/SCMWrapper cleanup (non-local commandset)
1484 deps_parent_content = CaptureSVN(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001485 ["cat",
1486 "%s/%s@%s" % (deps_parent_url_components[0],
1487 self._options.deps_file,
1488 deps_parent_url_components[1])],
1489 os.getcwd())
1490 sub_deps = self._ParseSolutionDeps(
1491 deps[d].module_name,
1492 FileRead(os.path.join(self._root_dir,
1493 deps[d].module_name,
1494 self._options.deps_file)),
1495 {})
1496 (url, rev) = GetURLAndRev(d, sub_deps[d])
1497 entries[d] = "%s@%d" % (url, rev)
1498
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +00001499 print(";".join(["%s,%s" % (x, entries[x]) for x in sorted(entries.keys())]))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001500
1501
1502## gclient commands.
1503
1504
1505def DoCleanup(options, args):
1506 """Handle the cleanup subcommand.
1507
1508 Raises:
1509 Error: if client isn't configured properly.
1510 """
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001511 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001512 if not client:
1513 raise Error("client not configured; see 'gclient config'")
1514 if options.verbose:
1515 # Print out the .gclient file. This is longer than if we just printed the
1516 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +00001517 print(client.ConfigContent())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001518 options.verbose = True
1519 return client.RunOnDeps('cleanup', args)
1520
1521
1522def DoConfig(options, args):
1523 """Handle the config subcommand.
1524
1525 Args:
1526 options: If options.spec set, a string providing contents of config file.
1527 args: The command line args. If spec is not set,
1528 then args[0] is a string URL to get for config file.
1529
1530 Raises:
1531 Error: on usage error
1532 """
1533 if len(args) < 1 and not options.spec:
1534 raise Error("required argument missing; see 'gclient help config'")
maruel@chromium.org0329e672009-05-13 18:41:04 +00001535 if os.path.exists(options.config_filename):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001536 raise Error("%s file already exists in the current directory" %
1537 options.config_filename)
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001538 client = GClient('.', options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001539 if options.spec:
1540 client.SetConfig(options.spec)
1541 else:
1542 # TODO(darin): it would be nice to be able to specify an alternate relpath
1543 # for the given URL.
maruel@chromium.org1ab7ffc2009-06-03 17:21:37 +00001544 base_url = args[0].rstrip('/')
1545 name = base_url.split("/")[-1]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001546 safesync_url = ""
1547 if len(args) > 1:
1548 safesync_url = args[1]
1549 client.SetDefaultConfig(name, base_url, safesync_url)
1550 client.SaveConfig()
1551
1552
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +00001553def DoExport(options, args):
1554 """Handle the export subcommand.
1555
1556 Raises:
1557 Error: on usage error
1558 """
1559 if len(args) != 1:
1560 raise Error("Need directory name")
1561 client = GClient.LoadCurrentConfig(options)
1562
1563 if not client:
1564 raise Error("client not configured; see 'gclient config'")
1565
1566 if options.verbose:
1567 # Print out the .gclient file. This is longer than if we just printed the
1568 # client dict, but more legible, and it might contain helpful comments.
1569 print(client.ConfigContent())
1570 return client.RunOnDeps('export', args)
1571
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001572def DoHelp(options, args):
1573 """Handle the help subcommand giving help for another subcommand.
1574
1575 Raises:
1576 Error: if the command is unknown.
1577 """
1578 if len(args) == 1 and args[0] in COMMAND_USAGE_TEXT:
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +00001579 print(COMMAND_USAGE_TEXT[args[0]])
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001580 else:
1581 raise Error("unknown subcommand '%s'; see 'gclient help'" % args[0])
1582
1583
1584def DoStatus(options, args):
1585 """Handle the status subcommand.
1586
1587 Raises:
1588 Error: if client isn't configured properly.
1589 """
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001590 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001591 if not client:
1592 raise Error("client not configured; see 'gclient config'")
1593 if options.verbose:
1594 # Print out the .gclient file. This is longer than if we just printed the
1595 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +00001596 print(client.ConfigContent())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001597 options.verbose = True
1598 return client.RunOnDeps('status', args)
1599
1600
1601def DoUpdate(options, args):
1602 """Handle the update and sync subcommands.
1603
1604 Raises:
1605 Error: if client isn't configured properly.
1606 """
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001607 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001608
1609 if not client:
1610 raise Error("client not configured; see 'gclient config'")
1611
1612 if not options.head:
1613 solutions = client.GetVar('solutions')
1614 if solutions:
1615 for s in solutions:
1616 if s.get('safesync_url', ''):
1617 # rip through revisions and make sure we're not over-riding
1618 # something that was explicitly passed
1619 has_key = False
1620 for r in options.revisions:
1621 if r.split('@')[0] == s['name']:
1622 has_key = True
1623 break
1624
1625 if not has_key:
1626 handle = urllib.urlopen(s['safesync_url'])
1627 rev = handle.read().strip()
1628 handle.close()
1629 if len(rev):
1630 options.revisions.append(s['name']+'@'+rev)
1631
1632 if options.verbose:
1633 # Print out the .gclient file. This is longer than if we just printed the
1634 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +00001635 print(client.ConfigContent())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001636 return client.RunOnDeps('update', args)
1637
1638
1639def DoDiff(options, args):
1640 """Handle the diff subcommand.
1641
1642 Raises:
1643 Error: if client isn't configured properly.
1644 """
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001645 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001646 if not client:
1647 raise Error("client not configured; see 'gclient config'")
1648 if options.verbose:
1649 # Print out the .gclient file. This is longer than if we just printed the
1650 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +00001651 print(client.ConfigContent())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001652 options.verbose = True
1653 return client.RunOnDeps('diff', args)
1654
1655
1656def DoRevert(options, args):
1657 """Handle the revert subcommand.
1658
1659 Raises:
1660 Error: if client isn't configured properly.
1661 """
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001662 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001663 if not client:
1664 raise Error("client not configured; see 'gclient config'")
1665 return client.RunOnDeps('revert', args)
1666
1667
1668def DoRunHooks(options, args):
1669 """Handle the runhooks subcommand.
1670
1671 Raises:
1672 Error: if client isn't configured properly.
1673 """
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001674 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001675 if not client:
1676 raise Error("client not configured; see 'gclient config'")
1677 if options.verbose:
1678 # Print out the .gclient file. This is longer than if we just printed the
1679 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +00001680 print(client.ConfigContent())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001681 return client.RunOnDeps('runhooks', args)
1682
1683
1684def DoRevInfo(options, args):
1685 """Handle the revinfo subcommand.
1686
1687 Raises:
1688 Error: if client isn't configured properly.
1689 """
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001690 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001691 if not client:
1692 raise Error("client not configured; see 'gclient config'")
1693 client.PrintRevInfo()
1694
1695
1696gclient_command_map = {
1697 "cleanup": DoCleanup,
1698 "config": DoConfig,
1699 "diff": DoDiff,
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +00001700 "export": DoExport,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001701 "help": DoHelp,
1702 "status": DoStatus,
1703 "sync": DoUpdate,
1704 "update": DoUpdate,
1705 "revert": DoRevert,
1706 "runhooks": DoRunHooks,
1707 "revinfo" : DoRevInfo,
1708}
1709
1710
1711def DispatchCommand(command, options, args, command_map=None):
1712 """Dispatches the appropriate subcommand based on command line arguments."""
1713 if command_map is None:
1714 command_map = gclient_command_map
1715
1716 if command in command_map:
1717 return command_map[command](options, args)
1718 else:
1719 raise Error("unknown subcommand '%s'; see 'gclient help'" % command)
1720
1721
1722def Main(argv):
1723 """Parse command line arguments and dispatch command."""
1724
1725 option_parser = optparse.OptionParser(usage=DEFAULT_USAGE_TEXT,
1726 version=__version__)
1727 option_parser.disable_interspersed_args()
1728 option_parser.add_option("", "--force", action="store_true", default=False,
1729 help=("(update/sync only) force update even "
1730 "for modules which haven't changed"))
evan@chromium.org67820ef2009-07-27 17:23:00 +00001731 option_parser.add_option("", "--nohooks", action="store_true", default=False,
1732 help=("(update/sync/revert only) prevent the hooks from "
1733 "running"))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001734 option_parser.add_option("", "--revision", action="append", dest="revisions",
1735 metavar="REV", default=[],
1736 help=("(update/sync only) sync to a specific "
1737 "revision, can be used multiple times for "
1738 "each solution, e.g. --revision=src@123, "
1739 "--revision=internal@32"))
1740 option_parser.add_option("", "--deps", default=None, dest="deps_os",
1741 metavar="OS_LIST",
1742 help=("(update/sync only) sync deps for the "
1743 "specified (comma-separated) platform(s); "
1744 "'all' will sync all platforms"))
1745 option_parser.add_option("", "--spec", default=None,
1746 help=("(config only) create a gclient file "
1747 "containing the provided string"))
1748 option_parser.add_option("", "--verbose", action="store_true", default=False,
1749 help="produce additional output for diagnostics")
1750 option_parser.add_option("", "--manually_grab_svn_rev", action="store_true",
1751 default=False,
1752 help="Skip svn up whenever possible by requesting "
1753 "actual HEAD revision from the repository")
1754 option_parser.add_option("", "--head", action="store_true", default=False,
1755 help=("skips any safesync_urls specified in "
1756 "configured solutions"))
ajwong@chromium.orgcdcee802009-06-23 15:30:42 +00001757 option_parser.add_option("", "--delete_unversioned_trees",
1758 action="store_true", default=False,
1759 help=("on update, delete any unexpected "
1760 "unversioned trees that are in the checkout"))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001761
1762 if len(argv) < 2:
1763 # Users don't need to be told to use the 'help' command.
1764 option_parser.print_help()
1765 return 1
1766 # Add manual support for --version as first argument.
1767 if argv[1] == '--version':
1768 option_parser.print_version()
1769 return 0
1770
1771 # Add manual support for --help as first argument.
1772 if argv[1] == '--help':
1773 argv[1] = 'help'
1774
1775 command = argv[1]
1776 options, args = option_parser.parse_args(argv[2:])
1777
1778 if len(argv) < 3 and command == "help":
1779 option_parser.print_help()
1780 return 0
1781
1782 # Files used for configuration and state saving.
1783 options.config_filename = os.environ.get("GCLIENT_FILE", ".gclient")
1784 options.entries_filename = ".gclient_entries"
1785 options.deps_file = "DEPS"
1786
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001787 options.platform = sys.platform
1788 return DispatchCommand(command, options, args)
1789
1790
1791if "__main__" == __name__:
1792 try:
1793 result = Main(sys.argv)
1794 except Error, e:
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +00001795 print >> sys.stderr, "Error: %s" % str(e)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001796 result = 1
1797 sys.exit(result)
1798
1799# vim: ts=2:sw=2:tw=80:et: