blob: fb95911dc42ff7894f12c1cea446a7f7beb2a690 [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
kbr@google.comab318592009-09-04 00:54:55 +000098 pack
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000099 revert
100 status
101 sync
102 update
103 runhooks
104 revinfo
105
106Options and extra arguments can be passed to invoked svn commands by
107appending them to the command line. Note that if the first such
108appended option starts with a dash (-) then the options must be
109preceded by -- to distinguish them from gclient options.
110
111For additional help on a subcommand or examples of usage, try
112 %prog help <subcommand>
113 %prog help files
114""")
115
116GENERIC_UPDATE_USAGE_TEXT = (
117 """Perform a checkout/update of the modules specified by the gclient
118configuration; see 'help config'. Unless --revision is specified,
119then the latest revision of the root solutions is checked out, with
120dependent submodule versions updated according to DEPS files.
121If --revision is specified, then the given revision is used in place
122of the latest, either for a single solution or for all solutions.
123Unless the --force option is provided, solutions and modules whose
124local revision matches the one to update (i.e., they have not changed
evan@chromium.org67820ef2009-07-27 17:23:00 +0000125in the repository) are *not* modified. Unless --nohooks is provided,
126the hooks are run.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000127This a synonym for 'gclient %(alias)s'
128
129usage: gclient %(cmd)s [options] [--] [svn update options/args]
130
131Valid options:
132 --force : force update even for unchanged modules
evan@chromium.org67820ef2009-07-27 17:23:00 +0000133 --nohooks : don't run the hooks after the update is complete
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000134 --revision REV : update/checkout all solutions with specified revision
135 --revision SOLUTION@REV : update given solution to specified revision
136 --deps PLATFORM(S) : sync deps for the given platform(s), or 'all'
137 --verbose : output additional diagnostics
maruel@chromium.orgb8b6b872009-06-30 18:50:56 +0000138 --head : update to latest revision, instead of last good revision
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000139
140Examples:
141 gclient %(cmd)s
142 update files from SVN according to current configuration,
143 *for modules which have changed since last update or sync*
144 gclient %(cmd)s --force
145 update files from SVN according to current configuration, for
146 all modules (useful for recovering files deleted from local copy)
147""")
148
149COMMAND_USAGE_TEXT = {
150 "cleanup":
151 """Clean up all working copies, using 'svn cleanup' for each module.
152Additional options and args may be passed to 'svn cleanup'.
153
154usage: cleanup [options] [--] [svn cleanup args/options]
155
156Valid options:
157 --verbose : output additional diagnostics
158""",
159 "config": """Create a .gclient file in the current directory; this
160specifies the configuration for further commands. After update/sync,
161top-level DEPS files in each module are read to determine dependent
162modules to operate on as well. If optional [url] parameter is
163provided, then configuration is read from a specified Subversion server
164URL. Otherwise, a --spec option must be provided.
165
166usage: config [option | url] [safesync url]
167
168Valid options:
169 --spec=GCLIENT_SPEC : contents of .gclient are read from string parameter.
170 *Note that due to Cygwin/Python brokenness, it
171 probably can't contain any newlines.*
172
173Examples:
174 gclient config https://gclient.googlecode.com/svn/trunk/gclient
175 configure a new client to check out gclient.py tool sources
176 gclient config --spec='solutions=[{"name":"gclient","""
177 '"url":"https://gclient.googlecode.com/svn/trunk/gclient",'
178 '"custom_deps":{}}]',
179 "diff": """Display the differences between two revisions of modules.
180(Does 'svn diff' for each checked out module and dependences.)
181Additional args and options to 'svn diff' can be passed after
182gclient options.
183
184usage: diff [options] [--] [svn args/options]
185
186Valid options:
187 --verbose : output additional diagnostics
188
189Examples:
190 gclient diff
191 simple 'svn diff' for configured client and dependences
192 gclient diff -- -x -b
193 use 'svn diff -x -b' to suppress whitespace-only differences
194 gclient diff -- -r HEAD -x -b
195 diff versus the latest version of each module
196""",
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +0000197 "export":
198 """Wrapper for svn export for all managed directories
199""",
kbr@google.comab318592009-09-04 00:54:55 +0000200 "pack":
201
202 """Generate a patch which can be applied at the root of the tree.
203Internally, runs 'svn diff' on each checked out module and
204dependencies, and performs minimal postprocessing of the output. The
205resulting patch is printed to stdout and can be applied to a freshly
206checked out tree via 'patch -p0 < patchfile'. Additional args and
207options to 'svn diff' can be passed after gclient options.
208
209usage: pack [options] [--] [svn args/options]
210
211Valid options:
212 --verbose : output additional diagnostics
213
214Examples:
215 gclient pack > patch.txt
216 generate simple patch for configured client and dependences
217 gclient pack -- -x -b > patch.txt
218 generate patch using 'svn diff -x -b' to suppress
219 whitespace-only differences
220 gclient pack -- -r HEAD -x -b > patch.txt
221 generate patch, diffing each file versus the latest version of
222 each module
223""",
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000224 "revert":
225 """Revert every file in every managed directory in the client view.
226
227usage: revert
228""",
229 "status":
230 """Show the status of client and dependent modules, using 'svn diff'
231for each module. Additional options and args may be passed to 'svn diff'.
232
233usage: status [options] [--] [svn diff args/options]
234
235Valid options:
236 --verbose : output additional diagnostics
evan@chromium.org67820ef2009-07-27 17:23:00 +0000237 --nohooks : don't run the hooks after the update is complete
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000238""",
239 "sync": GENERIC_UPDATE_USAGE_TEXT % {"cmd": "sync", "alias": "update"},
240 "update": GENERIC_UPDATE_USAGE_TEXT % {"cmd": "update", "alias": "sync"},
241 "help": """Describe the usage of this program or its subcommands.
242
243usage: help [options] [subcommand]
244
245Valid options:
246 --verbose : output additional diagnostics
247""",
248 "runhooks":
249 """Runs hooks for files that have been modified in the local working copy,
maruel@chromium.org5df6a462009-08-28 18:52:26 +0000250according to 'svn status'. Implies --force.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000251
252usage: runhooks [options]
253
254Valid options:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000255 --verbose : output additional diagnostics
256""",
257 "revinfo":
258 """Outputs source path, server URL and revision information for every
259dependency in all solutions (no local checkout required).
260
261usage: revinfo [options]
262""",
263}
264
265# parameterized by (solution_name, solution_url, safesync_url)
266DEFAULT_CLIENT_FILE_TEXT = (
267 """
268# An element of this array (a \"solution\") describes a repository directory
269# that will be checked out into your working copy. Each solution may
270# optionally define additional dependencies (via its DEPS file) to be
271# checked out alongside the solution's directory. A solution may also
272# specify custom dependencies (via the \"custom_deps\" property) that
273# override or augment the dependencies specified by the DEPS file.
274# If a \"safesync_url\" is specified, it is assumed to reference the location of
275# a text file which contains nothing but the last known good SCM revision to
276# sync against. It is fetched if specified and used unless --head is passed
277solutions = [
278 { \"name\" : \"%s\",
279 \"url\" : \"%s\",
280 \"custom_deps\" : {
281 # To use the trunk of a component instead of what's in DEPS:
282 #\"component\": \"https://svnserver/component/trunk/\",
283 # To exclude a component from your working copy:
284 #\"data/really_large_component\": None,
285 },
286 \"safesync_url\": \"%s\"
287 }
288]
289""")
290
291
292## Generic utils
293
maruel@chromium.orge105d8d2009-04-30 17:58:25 +0000294def ParseXML(output):
295 try:
296 return xml.dom.minidom.parseString(output)
297 except xml.parsers.expat.ExpatError:
298 return None
299
300
maruel@chromium.org483b0082009-05-07 02:57:14 +0000301def GetNamedNodeText(node, node_name):
302 child_nodes = node.getElementsByTagName(node_name)
303 if not child_nodes:
304 return None
305 assert len(child_nodes) == 1 and child_nodes[0].childNodes.length == 1
306 return child_nodes[0].firstChild.nodeValue
307
308
309def GetNodeNamedAttributeText(node, node_name, attribute_name):
310 child_nodes = node.getElementsByTagName(node_name)
311 if not child_nodes:
312 return None
313 assert len(child_nodes) == 1
314 return child_nodes[0].getAttribute(attribute_name)
315
316
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000317class Error(Exception):
318 """gclient exception class."""
319 pass
320
321class PrintableObject(object):
322 def __str__(self):
323 output = ''
324 for i in dir(self):
325 if i.startswith('__'):
326 continue
327 output += '%s = %s\n' % (i, str(getattr(self, i, '')))
328 return output
329
330
331def FileRead(filename):
332 content = None
333 f = open(filename, "rU")
334 try:
335 content = f.read()
336 finally:
337 f.close()
338 return content
339
340
341def FileWrite(filename, content):
342 f = open(filename, "w")
343 try:
344 f.write(content)
345 finally:
346 f.close()
347
348
349def RemoveDirectory(*path):
350 """Recursively removes a directory, even if it's marked read-only.
351
352 Remove the directory located at *path, if it exists.
353
354 shutil.rmtree() doesn't work on Windows if any of the files or directories
355 are read-only, which svn repositories and some .svn files are. We need to
356 be able to force the files to be writable (i.e., deletable) as we traverse
357 the tree.
358
359 Even with all this, Windows still sometimes fails to delete a file, citing
360 a permission error (maybe something to do with antivirus scans or disk
361 indexing). The best suggestion any of the user forums had was to wait a
362 bit and try again, so we do that too. It's hand-waving, but sometimes it
363 works. :/
364
365 On POSIX systems, things are a little bit simpler. The modes of the files
366 to be deleted doesn't matter, only the modes of the directories containing
367 them are significant. As the directory tree is traversed, each directory
368 has its mode set appropriately before descending into it. This should
369 result in the entire tree being removed, with the possible exception of
370 *path itself, because nothing attempts to change the mode of its parent.
371 Doing so would be hazardous, as it's not a directory slated for removal.
372 In the ordinary case, this is not a problem: for our purposes, the user
373 will never lack write permission on *path's parent.
374 """
375 file_path = os.path.join(*path)
376 if not os.path.exists(file_path):
377 return
378
379 if os.path.islink(file_path) or not os.path.isdir(file_path):
380 raise Error("RemoveDirectory asked to remove non-directory %s" % file_path)
381
382 has_win32api = False
383 if sys.platform == 'win32':
384 has_win32api = True
385 # Some people don't have the APIs installed. In that case we'll do without.
386 try:
387 win32api = __import__('win32api')
388 win32con = __import__('win32con')
389 except ImportError:
390 has_win32api = False
391 else:
392 # On POSIX systems, we need the x-bit set on the directory to access it,
393 # the r-bit to see its contents, and the w-bit to remove files from it.
394 # The actual modes of the files within the directory is irrelevant.
395 os.chmod(file_path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
396 for fn in os.listdir(file_path):
397 fullpath = os.path.join(file_path, fn)
398
399 # If fullpath is a symbolic link that points to a directory, isdir will
400 # be True, but we don't want to descend into that as a directory, we just
401 # want to remove the link. Check islink and treat links as ordinary files
402 # would be treated regardless of what they reference.
403 if os.path.islink(fullpath) or not os.path.isdir(fullpath):
404 if sys.platform == 'win32':
405 os.chmod(fullpath, stat.S_IWRITE)
406 if has_win32api:
407 win32api.SetFileAttributes(fullpath, win32con.FILE_ATTRIBUTE_NORMAL)
408 try:
409 os.remove(fullpath)
410 except OSError, e:
411 if e.errno != errno.EACCES or sys.platform != 'win32':
412 raise
413 print 'Failed to delete %s: trying again' % fullpath
414 time.sleep(0.1)
415 os.remove(fullpath)
416 else:
417 RemoveDirectory(fullpath)
418
419 if sys.platform == 'win32':
420 os.chmod(file_path, stat.S_IWRITE)
421 if has_win32api:
422 win32api.SetFileAttributes(file_path, win32con.FILE_ATTRIBUTE_NORMAL)
423 try:
424 os.rmdir(file_path)
425 except OSError, e:
426 if e.errno != errno.EACCES or sys.platform != 'win32':
427 raise
428 print 'Failed to remove %s: trying again' % file_path
429 time.sleep(0.1)
430 os.rmdir(file_path)
431
432
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000433def SubprocessCall(command, in_directory, fail_status=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000434 """Runs command, a list, in directory in_directory.
435
kbr@google.comab318592009-09-04 00:54:55 +0000436 This function wraps SubprocessCallAndFilter, but does not perform the
437 filtering functions. See that function for a more complete usage
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000438 description.
439 """
440 # Call subprocess and capture nothing:
kbr@google.comab318592009-09-04 00:54:55 +0000441 SubprocessCallAndFilter(command, in_directory, True, True, fail_status)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000442
443
kbr@google.comab318592009-09-04 00:54:55 +0000444def SubprocessCallAndFilter(command,
445 in_directory,
446 print_messages,
447 print_stdout,
448 fail_status=None, filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000449 """Runs command, a list, in directory in_directory.
450
kbr@google.comab318592009-09-04 00:54:55 +0000451 If print_messages is true, a message indicating what is being done
452 is printed to stdout. If print_stdout is true, the command's stdout
453 is also forwarded to stdout.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000454
kbr@google.comab318592009-09-04 00:54:55 +0000455 If a filter function is specified, it is expected to take a single
456 string argument, and it will be called with each line of the
457 subprocess's output. Each line has had the trailing newline character
458 trimmed.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000459
460 If the command fails, as indicated by a nonzero exit status, gclient will
461 exit with an exit status of fail_status. If fail_status is None (the
462 default), gclient will raise an Error exception.
463 """
464
kbr@google.comab318592009-09-04 00:54:55 +0000465 if print_messages:
466 print("\n________ running \'%s\' in \'%s\'"
467 % (' '.join(command), in_directory))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000468
469 # *Sigh*: Windows needs shell=True, or else it won't search %PATH% for the
470 # executable, but shell=True makes subprocess on Linux fail when it's called
471 # with a list because it only tries to execute the first item in the list.
472 kid = subprocess.Popen(command, bufsize=0, cwd=in_directory,
473 shell=(sys.platform == 'win32'), stdout=subprocess.PIPE)
474
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000475 # Also, we need to forward stdout to prevent weird re-ordering of output.
476 # This has to be done on a per byte basis to make sure it is not buffered:
477 # normally buffering is done for each line, but if svn requests input, no
478 # end-of-line character is output after the prompt and it would not show up.
479 in_byte = kid.stdout.read(1)
480 in_line = ""
481 while in_byte:
482 if in_byte != "\r":
kbr@google.comab318592009-09-04 00:54:55 +0000483 if print_stdout:
484 sys.stdout.write(in_byte)
485 if in_byte != "\n":
486 in_line += in_byte
487 if in_byte == "\n" and filter:
488 filter(in_line)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000489 in_line = ""
490 in_byte = kid.stdout.read(1)
491 rv = kid.wait()
492
493 if rv:
494 msg = "failed to run command: %s" % " ".join(command)
495
496 if fail_status != None:
497 print >>sys.stderr, msg
498 sys.exit(fail_status)
499
500 raise Error(msg)
501
502
503def IsUsingGit(root, paths):
504 """Returns True if we're using git to manage any of our checkouts.
505 |entries| is a list of paths to check."""
506 for path in paths:
507 if os.path.exists(os.path.join(root, path, '.git')):
508 return True
509 return False
510
511# -----------------------------------------------------------------------------
512# SVN utils:
513
514
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000515def RunSVN(args, in_directory):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000516 """Runs svn, sending output to stdout.
517
518 Args:
519 args: A sequence of command line parameters to be passed to svn.
520 in_directory: The directory where svn is to be run.
521
522 Raises:
523 Error: An error occurred while running the svn command.
524 """
525 c = [SVN_COMMAND]
526 c.extend(args)
527
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000528 SubprocessCall(c, in_directory)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000529
530
maruel@chromium.org5c3a2ff2009-05-12 19:28:55 +0000531def CaptureSVN(args, in_directory=None, print_error=True):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000532 """Runs svn, capturing output sent to stdout as a string.
533
534 Args:
535 args: A sequence of command line parameters to be passed to svn.
536 in_directory: The directory where svn is to be run.
537
538 Returns:
539 The output sent to stdout as a string.
540 """
541 c = [SVN_COMMAND]
542 c.extend(args)
543
544 # *Sigh*: Windows needs shell=True, or else it won't search %PATH% for
545 # the svn.exe executable, but shell=True makes subprocess on Linux fail
546 # when it's called with a list because it only tries to execute the
547 # first string ("svn").
maruel@chromium.org5c3a2ff2009-05-12 19:28:55 +0000548 stderr = None
maruel@chromium.org672343d2009-05-20 20:03:25 +0000549 if not print_error:
maruel@chromium.org5c3a2ff2009-05-12 19:28:55 +0000550 stderr = subprocess.PIPE
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000551 return subprocess.Popen(c,
552 cwd=in_directory,
553 shell=(sys.platform == 'win32'),
maruel@chromium.org5c3a2ff2009-05-12 19:28:55 +0000554 stdout=subprocess.PIPE,
555 stderr=stderr).communicate()[0]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000556
557
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000558def RunSVNAndGetFileList(args, in_directory, file_list):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000559 """Runs svn checkout, update, or status, output to stdout.
560
561 The first item in args must be either "checkout", "update", or "status".
562
563 svn's stdout is parsed to collect a list of files checked out or updated.
564 These files are appended to file_list. svn's stdout is also printed to
565 sys.stdout as in RunSVN.
566
567 Args:
568 args: A sequence of command line parameters to be passed to svn.
569 in_directory: The directory where svn is to be run.
570
571 Raises:
572 Error: An error occurred while running the svn command.
573 """
574 command = [SVN_COMMAND]
575 command.extend(args)
576
577 # svn update and svn checkout use the same pattern: the first three columns
578 # are for file status, property status, and lock status. This is followed
579 # by two spaces, and then the path to the file.
580 update_pattern = '^... (.*)$'
581
582 # The first three columns of svn status are the same as for svn update and
583 # svn checkout. The next three columns indicate addition-with-history,
584 # switch, and remote lock status. This is followed by one space, and then
585 # the path to the file.
586 status_pattern = '^...... (.*)$'
587
588 # args[0] must be a supported command. This will blow up if it's something
589 # else, which is good. Note that the patterns are only effective when
590 # these commands are used in their ordinary forms, the patterns are invalid
591 # for "svn status --show-updates", for example.
592 pattern = {
593 'checkout': update_pattern,
594 'status': status_pattern,
595 'update': update_pattern,
596 }[args[0]]
597
kbr@google.comab318592009-09-04 00:54:55 +0000598 compiled_pattern = re.compile(pattern)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000599
kbr@google.comab318592009-09-04 00:54:55 +0000600 def CaptureMatchingLines(line):
601 match = compiled_pattern.search(line)
602 if match:
603 file_list.append(match.group(1))
604
605 RunSVNAndFilterOutput(args,
606 in_directory,
607 True,
608 True,
609 CaptureMatchingLines)
610
611def RunSVNAndFilterOutput(args,
612 in_directory,
613 print_messages,
614 print_stdout,
615 filter):
616 """Runs svn checkout, update, status, or diff, optionally outputting
617 to stdout.
618
619 The first item in args must be either "checkout", "update",
620 "status", or "diff".
621
622 svn's stdout is passed line-by-line to the given filter function. If
623 print_stdout is true, it is also printed to sys.stdout as in RunSVN.
624
625 Args:
626 args: A sequence of command line parameters to be passed to svn.
627 in_directory: The directory where svn is to be run.
628 print_messages: Whether to print status messages to stdout about
629 which Subversion commands are being run.
630 print_stdout: Whether to forward Subversion's output to stdout.
631 filter: A function taking one argument (a string) which will be
632 passed each line (with the ending newline character removed) of
633 Subversion's output for filtering.
634
635 Raises:
636 Error: An error occurred while running the svn command.
637 """
638 command = [SVN_COMMAND]
639 command.extend(args)
640
641 SubprocessCallAndFilter(command,
642 in_directory,
643 print_messages,
644 print_stdout,
645 filter=filter)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000646
maruel@chromium.org5c3a2ff2009-05-12 19:28:55 +0000647def CaptureSVNInfo(relpath, in_directory=None, print_error=True):
maruel@chromium.org2dc8a4d2009-05-11 12:41:20 +0000648 """Returns a dictionary from the svn info output for the given file.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000649
650 Args:
651 relpath: The directory where the working copy resides relative to
652 the directory given by in_directory.
653 in_directory: The directory where svn is to be run.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000654 """
maruel@chromium.org5c3a2ff2009-05-12 19:28:55 +0000655 output = CaptureSVN(["info", "--xml", relpath], in_directory, print_error)
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000656 dom = ParseXML(output)
maruel@chromium.org2dc8a4d2009-05-11 12:41:20 +0000657 result = {}
maruel@chromium.org483b0082009-05-07 02:57:14 +0000658 if dom:
maruel@chromium.org2dc8a4d2009-05-11 12:41:20 +0000659 def C(item, f):
660 if item is not None: return f(item)
maruel@chromium.org483b0082009-05-07 02:57:14 +0000661 # /info/entry/
662 # url
663 # reposityory/(root|uuid)
664 # wc-info/(schedule|depth)
665 # commit/(author|date)
666 # str() the results because they may be returned as Unicode, which
667 # interferes with the higher layers matching up things in the deps
668 # dictionary.
maruel@chromium.org2dc8a4d2009-05-11 12:41:20 +0000669 # TODO(maruel): Fix at higher level instead (!)
670 result['Repository Root'] = C(GetNamedNodeText(dom, 'root'), str)
671 result['URL'] = C(GetNamedNodeText(dom, 'url'), str)
672 result['UUID'] = C(GetNamedNodeText(dom, 'uuid'), str)
673 result['Revision'] = C(GetNodeNamedAttributeText(dom, 'entry', 'revision'),
674 int)
675 result['Node Kind'] = C(GetNodeNamedAttributeText(dom, 'entry', 'kind'),
676 str)
677 result['Schedule'] = C(GetNamedNodeText(dom, 'schedule'), str)
678 result['Path'] = C(GetNodeNamedAttributeText(dom, 'entry', 'path'), str)
679 result['Copied From URL'] = C(GetNamedNodeText(dom, 'copy-from-url'), str)
680 result['Copied From Rev'] = C(GetNamedNodeText(dom, 'copy-from-rev'), str)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000681 return result
682
683
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000684def CaptureSVNHeadRevision(url):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000685 """Get the head revision of a SVN repository.
686
687 Returns:
688 Int head revision
689 """
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000690 info = CaptureSVN(["info", "--xml", url], os.getcwd())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000691 dom = xml.dom.minidom.parseString(info)
692 return int(dom.getElementsByTagName('entry')[0].getAttribute('revision'))
693
694
maruel@chromium.org4810a962009-05-12 21:03:34 +0000695def CaptureSVNStatus(files):
696 """Returns the svn 1.5 svn status emulated output.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000697
maruel@chromium.org4810a962009-05-12 21:03:34 +0000698 @files can be a string (one file) or a list of files.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000699
maruel@chromium.org4810a962009-05-12 21:03:34 +0000700 Returns an array of (status, file) tuples."""
701 command = ["status", "--xml"]
702 if not files:
703 pass
704 elif isinstance(files, basestring):
705 command.append(files)
706 else:
707 command.extend(files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000708
maruel@chromium.org4810a962009-05-12 21:03:34 +0000709 status_letter = {
710 None: ' ',
711 '': ' ',
712 'added': 'A',
713 'conflicted': 'C',
714 'deleted': 'D',
715 'external': 'X',
716 'ignored': 'I',
717 'incomplete': '!',
718 'merged': 'G',
719 'missing': '!',
720 'modified': 'M',
721 'none': ' ',
722 'normal': ' ',
723 'obstructed': '~',
724 'replaced': 'R',
725 'unversioned': '?',
726 }
727 dom = ParseXML(CaptureSVN(command))
maruel@chromium.orge105d8d2009-04-30 17:58:25 +0000728 results = []
729 if dom:
730 # /status/target/entry/(wc-status|commit|author|date)
731 for target in dom.getElementsByTagName('target'):
732 base_path = target.getAttribute('path')
733 for entry in target.getElementsByTagName('entry'):
734 file = entry.getAttribute('path')
735 wc_status = entry.getElementsByTagName('wc-status')
736 assert len(wc_status) == 1
737 # Emulate svn 1.5 status ouput...
738 statuses = [' ' for i in range(7)]
739 # Col 0
740 xml_item_status = wc_status[0].getAttribute('item')
maruel@chromium.org4810a962009-05-12 21:03:34 +0000741 if xml_item_status in status_letter:
742 statuses[0] = status_letter[xml_item_status]
maruel@chromium.orge105d8d2009-04-30 17:58:25 +0000743 else:
744 raise Exception('Unknown item status "%s"; please implement me!' %
745 xml_item_status)
746 # Col 1
747 xml_props_status = wc_status[0].getAttribute('props')
748 if xml_props_status == 'modified':
749 statuses[1] = 'M'
750 elif xml_props_status == 'conflicted':
751 statuses[1] = 'C'
752 elif (not xml_props_status or xml_props_status == 'none' or
753 xml_props_status == 'normal'):
754 pass
755 else:
756 raise Exception('Unknown props status "%s"; please implement me!' %
757 xml_props_status)
maruel@chromium.orgedd27d12009-05-01 17:46:56 +0000758 # Col 2
759 if wc_status[0].getAttribute('wc-locked') == 'true':
760 statuses[2] = 'L'
maruel@chromium.orge105d8d2009-04-30 17:58:25 +0000761 # Col 3
762 if wc_status[0].getAttribute('copied') == 'true':
763 statuses[3] = '+'
maruel@chromium.org4810a962009-05-12 21:03:34 +0000764 item = (''.join(statuses), file)
maruel@chromium.orge105d8d2009-04-30 17:58:25 +0000765 results.append(item)
766 return results
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000767
768
769### SCM abstraction layer
770
771
772class SCMWrapper(object):
773 """Add necessary glue between all the supported SCM.
774
775 This is the abstraction layer to bind to different SCM. Since currently only
776 subversion is supported, a lot of subersionism remains. This can be sorted out
777 once another SCM is supported."""
778 def __init__(self, url=None, root_dir=None, relpath=None,
779 scm_name='svn'):
780 # TODO(maruel): Deduce the SCM from the url.
781 self.scm_name = scm_name
782 self.url = url
783 self._root_dir = root_dir
784 if self._root_dir:
maruel@chromium.orge105d8d2009-04-30 17:58:25 +0000785 self._root_dir = self._root_dir.replace('/', os.sep)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000786 self.relpath = relpath
787 if self.relpath:
maruel@chromium.orge105d8d2009-04-30 17:58:25 +0000788 self.relpath = self.relpath.replace('/', os.sep)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000789
790 def FullUrlForRelativeUrl(self, url):
791 # Find the forth '/' and strip from there. A bit hackish.
792 return '/'.join(self.url.split('/')[:4]) + url
793
794 def RunCommand(self, command, options, args, file_list=None):
795 # file_list will have all files that are modified appended to it.
796
797 if file_list == None:
798 file_list = []
799
800 commands = {
801 'cleanup': self.cleanup,
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +0000802 'export': self.export,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000803 'update': self.update,
804 'revert': self.revert,
805 'status': self.status,
806 'diff': self.diff,
kbr@google.comab318592009-09-04 00:54:55 +0000807 'pack': self.pack,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000808 'runhooks': self.status,
809 }
810
811 if not command in commands:
812 raise Error('Unknown command %s' % command)
813
814 return commands[command](options, args, file_list)
815
816 def cleanup(self, options, args, file_list):
817 """Cleanup working copy."""
818 command = ['cleanup']
819 command.extend(args)
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000820 RunSVN(command, os.path.join(self._root_dir, self.relpath))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000821
822 def diff(self, options, args, file_list):
823 # NOTE: This function does not currently modify file_list.
824 command = ['diff']
825 command.extend(args)
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000826 RunSVN(command, os.path.join(self._root_dir, self.relpath))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000827
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +0000828 def export(self, options, args, file_list):
829 assert len(args) == 1
830 export_path = os.path.abspath(os.path.join(args[0], self.relpath))
831 try:
832 os.makedirs(export_path)
833 except OSError:
834 pass
835 assert os.path.exists(export_path)
836 command = ['export', '--force', '.']
837 command.append(export_path)
838 RunSVN(command, os.path.join(self._root_dir, self.relpath))
839
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000840 def update(self, options, args, file_list):
841 """Runs SCM to update or transparently checkout the working copy.
842
843 All updated files will be appended to file_list.
844
845 Raises:
846 Error: if can't get URL for relative path.
847 """
848 # Only update if git is not controlling the directory.
maruel@chromium.org8626ff72009-05-13 02:57:02 +0000849 checkout_path = os.path.join(self._root_dir, self.relpath)
maruel@chromium.org0329e672009-05-13 18:41:04 +0000850 git_path = os.path.join(self._root_dir, self.relpath, '.git')
851 if os.path.exists(git_path):
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000852 print("________ found .git directory; skipping %s" % self.relpath)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000853 return
854
855 if args:
856 raise Error("Unsupported argument(s): %s" % ",".join(args))
857
858 url = self.url
859 components = url.split("@")
860 revision = None
861 forced_revision = False
862 if options.revision:
863 # Override the revision number.
864 url = '%s@%s' % (components[0], str(options.revision))
865 revision = int(options.revision)
866 forced_revision = True
867 elif len(components) == 2:
868 revision = int(components[1])
869 forced_revision = True
870
871 rev_str = ""
872 if revision:
873 rev_str = ' at %d' % revision
874
maruel@chromium.org0329e672009-05-13 18:41:04 +0000875 if not os.path.exists(checkout_path):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000876 # We need to checkout.
maruel@chromium.org8626ff72009-05-13 02:57:02 +0000877 command = ['checkout', url, checkout_path]
maruel@chromium.orgedd27d12009-05-01 17:46:56 +0000878 if revision:
879 command.extend(['--revision', str(revision)])
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000880 RunSVNAndGetFileList(command, self._root_dir, file_list)
maruel@chromium.orgedd27d12009-05-01 17:46:56 +0000881 return
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000882
883 # Get the existing scm url and the revision number of the current checkout.
maruel@chromium.org8626ff72009-05-13 02:57:02 +0000884 from_info = CaptureSVNInfo(os.path.join(checkout_path, '.'), '.')
maruel@chromium.org1998c6d2009-05-15 12:38:12 +0000885 if not from_info:
886 raise Error("Can't update/checkout %r if an unversioned directory is "
887 "present. Delete the directory and try again." %
888 checkout_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000889
890 if options.manually_grab_svn_rev:
891 # Retrieve the current HEAD version because svn is slow at null updates.
892 if not revision:
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000893 from_info_live = CaptureSVNInfo(from_info['URL'], '.')
maruel@chromium.org2dc8a4d2009-05-11 12:41:20 +0000894 revision = int(from_info_live['Revision'])
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000895 rev_str = ' at %d' % revision
896
maruel@chromium.org2dc8a4d2009-05-11 12:41:20 +0000897 if from_info['URL'] != components[0]:
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000898 to_info = CaptureSVNInfo(url, '.')
maruel@chromium.org8626ff72009-05-13 02:57:02 +0000899 can_switch = ((from_info['Repository Root'] != to_info['Repository Root'])
900 and (from_info['UUID'] == to_info['UUID']))
901 if can_switch:
902 print("\n_____ relocating %s to a new checkout" % self.relpath)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000903 # We have different roots, so check if we can switch --relocate.
904 # Subversion only permits this if the repository UUIDs match.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000905 # Perform the switch --relocate, then rewrite the from_url
906 # to reflect where we "are now." (This is the same way that
907 # Subversion itself handles the metadata when switch --relocate
908 # is used.) This makes the checks below for whether we
909 # can update to a revision or have to switch to a different
910 # branch work as expected.
911 # TODO(maruel): TEST ME !
maruel@chromium.org2dc8a4d2009-05-11 12:41:20 +0000912 command = ["switch", "--relocate",
913 from_info['Repository Root'],
914 to_info['Repository Root'],
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000915 self.relpath]
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000916 RunSVN(command, self._root_dir)
maruel@chromium.org2dc8a4d2009-05-11 12:41:20 +0000917 from_info['URL'] = from_info['URL'].replace(
918 from_info['Repository Root'],
919 to_info['Repository Root'])
maruel@chromium.org8626ff72009-05-13 02:57:02 +0000920 else:
921 if CaptureSVNStatus(checkout_path):
922 raise Error("Can't switch the checkout to %s; UUID don't match and "
923 "there is local changes in %s. Delete the directory and "
924 "try again." % (url, checkout_path))
925 # Ok delete it.
926 print("\n_____ switching %s to a new checkout" % self.relpath)
927 RemoveDirectory(checkout_path)
928 # We need to checkout.
929 command = ['checkout', url, checkout_path]
930 if revision:
931 command.extend(['--revision', str(revision)])
932 RunSVNAndGetFileList(command, self._root_dir, file_list)
933 return
phajdan.jr@chromium.org71b40682009-07-31 23:40:09 +0000934
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000935
936 # If the provided url has a revision number that matches the revision
937 # number of the existing directory, then we don't need to bother updating.
maruel@chromium.org2dc8a4d2009-05-11 12:41:20 +0000938 if not options.force and from_info['Revision'] == revision:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000939 if options.verbose or not forced_revision:
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000940 print("\n_____ %s%s" % (self.relpath, rev_str))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000941 return
942
maruel@chromium.org8626ff72009-05-13 02:57:02 +0000943 command = ["update", checkout_path]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000944 if revision:
945 command.extend(['--revision', str(revision)])
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000946 RunSVNAndGetFileList(command, self._root_dir, file_list)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000947
948 def revert(self, options, args, file_list):
949 """Reverts local modifications. Subversion specific.
950
951 All reverted files will be appended to file_list, even if Subversion
952 doesn't know about them.
953 """
954 path = os.path.join(self._root_dir, self.relpath)
955 if not os.path.isdir(path):
maruel@chromium.orgedd27d12009-05-01 17:46:56 +0000956 # svn revert won't work if the directory doesn't exist. It needs to
957 # checkout instead.
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000958 print("\n_____ %s is missing, synching instead" % self.relpath)
maruel@chromium.orgedd27d12009-05-01 17:46:56 +0000959 # Don't reuse the args.
960 return self.update(options, [], file_list)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000961
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000962 files = CaptureSVNStatus(path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000963 # Batch the command.
964 files_to_revert = []
965 for file in files:
maruel@chromium.org4810a962009-05-12 21:03:34 +0000966 file_path = os.path.join(path, file[1])
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000967 print(file_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000968 # Unversioned file or unexpected unversioned file.
maruel@chromium.org4810a962009-05-12 21:03:34 +0000969 if file[0][0] in ('?', '~'):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000970 # Remove extraneous file. Also remove unexpected unversioned
971 # directories. svn won't touch them but we want to delete these.
972 file_list.append(file_path)
973 try:
974 os.remove(file_path)
975 except EnvironmentError:
976 RemoveDirectory(file_path)
977
maruel@chromium.org4810a962009-05-12 21:03:34 +0000978 if file[0][0] != '?':
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000979 # For any other status, svn revert will work.
980 file_list.append(file_path)
maruel@chromium.org4810a962009-05-12 21:03:34 +0000981 files_to_revert.append(file[1])
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000982
983 # Revert them all at once.
984 if files_to_revert:
985 accumulated_paths = []
986 accumulated_length = 0
987 command = ['revert']
988 for p in files_to_revert:
989 # Some shell have issues with command lines too long.
990 if accumulated_length and accumulated_length + len(p) > 3072:
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000991 RunSVN(command + accumulated_paths,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000992 os.path.join(self._root_dir, self.relpath))
993 accumulated_paths = []
994 accumulated_length = 0
995 else:
996 accumulated_paths.append(p)
997 accumulated_length += len(p)
998 if accumulated_paths:
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000999 RunSVN(command + accumulated_paths,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001000 os.path.join(self._root_dir, self.relpath))
1001
1002 def status(self, options, args, file_list):
1003 """Display status information."""
maruel@chromium.orgedd27d12009-05-01 17:46:56 +00001004 path = os.path.join(self._root_dir, self.relpath)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001005 command = ['status']
1006 command.extend(args)
maruel@chromium.orgedd27d12009-05-01 17:46:56 +00001007 if not os.path.isdir(path):
1008 # svn status won't work if the directory doesn't exist.
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +00001009 print("\n________ couldn't run \'%s\' in \'%s\':\nThe directory "
1010 "does not exist."
1011 % (' '.join(command), path))
maruel@chromium.orgedd27d12009-05-01 17:46:56 +00001012 # There's no file list to retrieve.
1013 else:
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +00001014 RunSVNAndGetFileList(command, path, file_list)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001015
kbr@google.comab318592009-09-04 00:54:55 +00001016 def pack(self, options, args, file_list):
1017 """Generates a patch file which can be applied to the root of the
1018 repository."""
1019 path = os.path.join(self._root_dir, self.relpath)
1020 command = ['diff']
1021 command.extend(args)
1022 # Simple class which tracks which file is being diffed and
1023 # replaces instances of its file name in the original and
1024 # working copy lines of the svn diff output.
1025 class DiffFilterer(object):
1026 index_string = "Index: "
1027 original_prefix = "--- "
1028 working_prefix = "+++ "
1029
1030 def __init__(self, relpath):
1031 # Note that we always use '/' as the path separator to be
1032 # consistent with svn's cygwin-style output on Windows
1033 self._relpath = relpath.replace("\\", "/")
1034 self._current_file = ""
1035 self._replacement_file = ""
1036
1037 def SetCurrentFile(self, file):
1038 self._current_file = file
1039 # Note that we always use '/' as the path separator to be
1040 # consistent with svn's cygwin-style output on Windows
1041 self._replacement_file = self._relpath + '/' + file
1042
1043 def ReplaceAndPrint(self, line):
1044 print(line.replace(self._current_file, self._replacement_file))
1045
1046 def Filter(self, line):
1047 if (line.startswith(self.index_string)):
1048 self.SetCurrentFile(line[len(self.index_string):])
1049 self.ReplaceAndPrint(line)
1050 else:
1051 if (line.startswith(self.original_prefix) or
1052 line.startswith(self.working_prefix)):
1053 self.ReplaceAndPrint(line)
1054 else:
1055 print line
1056
1057 filterer = DiffFilterer(self.relpath)
1058 RunSVNAndFilterOutput(command, path, False, False, filterer.Filter)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001059
1060## GClient implementation.
1061
1062
1063class GClient(object):
1064 """Object that represent a gclient checkout."""
1065
1066 supported_commands = [
kbr@google.comab318592009-09-04 00:54:55 +00001067 'cleanup', 'diff', 'export', 'pack', 'revert', 'status', 'update',
1068 'runhooks'
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001069 ]
1070
1071 def __init__(self, root_dir, options):
1072 self._root_dir = root_dir
1073 self._options = options
1074 self._config_content = None
1075 self._config_dict = {}
1076 self._deps_hooks = []
1077
1078 def SetConfig(self, content):
1079 self._config_dict = {}
1080 self._config_content = content
skylined@chromium.orgdf0032c2009-05-29 10:43:56 +00001081 try:
1082 exec(content, self._config_dict)
1083 except SyntaxError, e:
1084 try:
1085 # Try to construct a human readable error message
1086 error_message = [
1087 'There is a syntax error in your configuration file.',
1088 'Line #%s, character %s:' % (e.lineno, e.offset),
1089 '"%s"' % re.sub(r'[\r\n]*$', '', e.text) ]
1090 except:
1091 # Something went wrong, re-raise the original exception
1092 raise e
1093 else:
1094 # Raise a new exception with the human readable message:
1095 raise Error('\n'.join(error_message))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001096
1097 def SaveConfig(self):
1098 FileWrite(os.path.join(self._root_dir, self._options.config_filename),
1099 self._config_content)
1100
1101 def _LoadConfig(self):
1102 client_source = FileRead(os.path.join(self._root_dir,
1103 self._options.config_filename))
1104 self.SetConfig(client_source)
1105
1106 def ConfigContent(self):
1107 return self._config_content
1108
1109 def GetVar(self, key, default=None):
1110 return self._config_dict.get(key, default)
1111
1112 @staticmethod
1113 def LoadCurrentConfig(options, from_dir=None):
1114 """Searches for and loads a .gclient file relative to the current working
1115 dir.
1116
1117 Returns:
1118 A dict representing the contents of the .gclient file or an empty dict if
1119 the .gclient file doesn't exist.
1120 """
1121 if not from_dir:
1122 from_dir = os.curdir
1123 path = os.path.realpath(from_dir)
maruel@chromium.org0329e672009-05-13 18:41:04 +00001124 while not os.path.exists(os.path.join(path, options.config_filename)):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001125 next = os.path.split(path)
1126 if not next[1]:
1127 return None
1128 path = next[0]
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001129 client = GClient(path, options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001130 client._LoadConfig()
1131 return client
1132
1133 def SetDefaultConfig(self, solution_name, solution_url, safesync_url):
1134 self.SetConfig(DEFAULT_CLIENT_FILE_TEXT % (
1135 solution_name, solution_url, safesync_url
1136 ))
1137
1138 def _SaveEntries(self, entries):
1139 """Creates a .gclient_entries file to record the list of unique checkouts.
1140
1141 The .gclient_entries file lives in the same directory as .gclient.
1142
1143 Args:
1144 entries: A sequence of solution names.
1145 """
1146 text = "entries = [\n"
1147 for entry in entries:
1148 text += " \"%s\",\n" % entry
1149 text += "]\n"
1150 FileWrite(os.path.join(self._root_dir, self._options.entries_filename),
1151 text)
1152
1153 def _ReadEntries(self):
1154 """Read the .gclient_entries file for the given client.
1155
1156 Args:
1157 client: The client for which the entries file should be read.
1158
1159 Returns:
1160 A sequence of solution names, which will be empty if there is the
1161 entries file hasn't been created yet.
1162 """
1163 scope = {}
1164 filename = os.path.join(self._root_dir, self._options.entries_filename)
maruel@chromium.org0329e672009-05-13 18:41:04 +00001165 if not os.path.exists(filename):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001166 return []
1167 exec(FileRead(filename), scope)
1168 return scope["entries"]
1169
1170 class FromImpl:
1171 """Used to implement the From syntax."""
1172
1173 def __init__(self, module_name):
1174 self.module_name = module_name
1175
1176 def __str__(self):
1177 return 'From("%s")' % self.module_name
1178
1179 class _VarImpl:
1180 def __init__(self, custom_vars, local_scope):
1181 self._custom_vars = custom_vars
1182 self._local_scope = local_scope
1183
1184 def Lookup(self, var_name):
1185 """Implements the Var syntax."""
1186 if var_name in self._custom_vars:
1187 return self._custom_vars[var_name]
1188 elif var_name in self._local_scope.get("vars", {}):
1189 return self._local_scope["vars"][var_name]
1190 raise Error("Var is not defined: %s" % var_name)
1191
1192 def _ParseSolutionDeps(self, solution_name, solution_deps_content,
1193 custom_vars):
1194 """Parses the DEPS file for the specified solution.
1195
1196 Args:
1197 solution_name: The name of the solution to query.
1198 solution_deps_content: Content of the DEPS file for the solution
1199 custom_vars: A dict of vars to override any vars defined in the DEPS file.
1200
1201 Returns:
1202 A dict mapping module names (as relative paths) to URLs or an empty
1203 dict if the solution does not have a DEPS file.
1204 """
1205 # Skip empty
1206 if not solution_deps_content:
1207 return {}
1208 # Eval the content
1209 local_scope = {}
1210 var = self._VarImpl(custom_vars, local_scope)
1211 global_scope = {"From": self.FromImpl, "Var": var.Lookup, "deps_os": {}}
1212 exec(solution_deps_content, global_scope, local_scope)
1213 deps = local_scope.get("deps", {})
1214
1215 # load os specific dependencies if defined. these dependencies may
1216 # override or extend the values defined by the 'deps' member.
1217 if "deps_os" in local_scope:
1218 deps_os_choices = {
1219 "win32": "win",
1220 "win": "win",
1221 "cygwin": "win",
1222 "darwin": "mac",
1223 "mac": "mac",
1224 "unix": "unix",
1225 "linux": "unix",
1226 "linux2": "unix",
1227 }
1228
1229 if self._options.deps_os is not None:
1230 deps_to_include = self._options.deps_os.split(",")
1231 if "all" in deps_to_include:
1232 deps_to_include = deps_os_choices.values()
1233 else:
1234 deps_to_include = [deps_os_choices.get(self._options.platform, "unix")]
1235
1236 deps_to_include = set(deps_to_include)
1237 for deps_os_key in deps_to_include:
1238 os_deps = local_scope["deps_os"].get(deps_os_key, {})
1239 if len(deps_to_include) > 1:
1240 # Ignore any overrides when including deps for more than one
1241 # platform, so we collect the broadest set of dependencies available.
1242 # We may end up with the wrong revision of something for our
1243 # platform, but this is the best we can do.
1244 deps.update([x for x in os_deps.items() if not x[0] in deps])
1245 else:
1246 deps.update(os_deps)
1247
1248 if 'hooks' in local_scope:
1249 self._deps_hooks.extend(local_scope['hooks'])
1250
1251 # If use_relative_paths is set in the DEPS file, regenerate
1252 # the dictionary using paths relative to the directory containing
1253 # the DEPS file.
1254 if local_scope.get('use_relative_paths'):
1255 rel_deps = {}
1256 for d, url in deps.items():
1257 # normpath is required to allow DEPS to use .. in their
1258 # dependency local path.
1259 rel_deps[os.path.normpath(os.path.join(solution_name, d))] = url
1260 return rel_deps
1261 else:
1262 return deps
1263
1264 def _ParseAllDeps(self, solution_urls, solution_deps_content):
1265 """Parse the complete list of dependencies for the client.
1266
1267 Args:
1268 solution_urls: A dict mapping module names (as relative paths) to URLs
1269 corresponding to the solutions specified by the client. This parameter
1270 is passed as an optimization.
1271 solution_deps_content: A dict mapping module names to the content
1272 of their DEPS files
1273
1274 Returns:
1275 A dict mapping module names (as relative paths) to URLs corresponding
1276 to the entire set of dependencies to checkout for the given client.
1277
1278 Raises:
1279 Error: If a dependency conflicts with another dependency or of a solution.
1280 """
1281 deps = {}
1282 for solution in self.GetVar("solutions"):
1283 custom_vars = solution.get("custom_vars", {})
1284 solution_deps = self._ParseSolutionDeps(
1285 solution["name"],
1286 solution_deps_content[solution["name"]],
1287 custom_vars)
1288
1289 # If a line is in custom_deps, but not in the solution, we want to append
1290 # this line to the solution.
1291 if "custom_deps" in solution:
1292 for d in solution["custom_deps"]:
1293 if d not in solution_deps:
1294 solution_deps[d] = solution["custom_deps"][d]
1295
1296 for d in solution_deps:
1297 if "custom_deps" in solution and d in solution["custom_deps"]:
1298 # Dependency is overriden.
1299 url = solution["custom_deps"][d]
1300 if url is None:
1301 continue
1302 else:
1303 url = solution_deps[d]
1304 # if we have a From reference dependent on another solution, then
1305 # just skip the From reference. When we pull deps for the solution,
1306 # we will take care of this dependency.
1307 #
1308 # If multiple solutions all have the same From reference, then we
1309 # should only add one to our list of dependencies.
1310 if type(url) != str:
1311 if url.module_name in solution_urls:
1312 # Already parsed.
1313 continue
1314 if d in deps and type(deps[d]) != str:
1315 if url.module_name == deps[d].module_name:
1316 continue
1317 else:
1318 parsed_url = urlparse.urlparse(url)
1319 scheme = parsed_url[0]
1320 if not scheme:
1321 # A relative url. Fetch the real base.
1322 path = parsed_url[2]
1323 if path[0] != "/":
1324 raise Error(
1325 "relative DEPS entry \"%s\" must begin with a slash" % d)
1326 # Create a scm just to query the full url.
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001327 scm = SCMWrapper(solution["url"], self._root_dir, None)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001328 url = scm.FullUrlForRelativeUrl(url)
1329 if d in deps and deps[d] != url:
1330 raise Error(
1331 "Solutions have conflicting versions of dependency \"%s\"" % d)
1332 if d in solution_urls and solution_urls[d] != url:
1333 raise Error(
1334 "Dependency \"%s\" conflicts with specified solution" % d)
1335 # Grab the dependency.
1336 deps[d] = url
1337 return deps
1338
phajdan.jr@chromium.org71b40682009-07-31 23:40:09 +00001339 def _RunHookAction(self, hook_dict, matching_file_list):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001340 """Runs the action from a single hook.
1341 """
1342 command = hook_dict['action'][:]
1343 if command[0] == 'python':
1344 # If the hook specified "python" as the first item, the action is a
1345 # Python script. Run it by starting a new copy of the same
1346 # interpreter.
1347 command[0] = sys.executable
1348
phajdan.jr@chromium.org71b40682009-07-31 23:40:09 +00001349 if '$matching_files' in command:
phajdan.jr@chromium.org68f2e092009-08-06 17:05:35 +00001350 splice_index = command.index('$matching_files')
1351 command[splice_index:splice_index + 1] = matching_file_list
phajdan.jr@chromium.org71b40682009-07-31 23:40:09 +00001352
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001353 # Use a discrete exit status code of 2 to indicate that a hook action
1354 # failed. Users of this script may wish to treat hook action failures
1355 # differently from VC failures.
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +00001356 SubprocessCall(command, self._root_dir, fail_status=2)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001357
1358 def _RunHooks(self, command, file_list, is_using_git):
1359 """Evaluates all hooks, running actions as needed.
1360 """
1361 # Hooks only run for these command types.
1362 if not command in ('update', 'revert', 'runhooks'):
1363 return
1364
evan@chromium.org67820ef2009-07-27 17:23:00 +00001365 # Hooks only run when --nohooks is not specified
1366 if self._options.nohooks:
1367 return
1368
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001369 # Get any hooks from the .gclient file.
1370 hooks = self.GetVar("hooks", [])
1371 # Add any hooks found in DEPS files.
1372 hooks.extend(self._deps_hooks)
1373
1374 # If "--force" was specified, run all hooks regardless of what files have
1375 # changed. If the user is using git, then we don't know what files have
1376 # changed so we always run all hooks.
1377 if self._options.force or is_using_git:
1378 for hook_dict in hooks:
phajdan.jr@chromium.org71b40682009-07-31 23:40:09 +00001379 self._RunHookAction(hook_dict, [])
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001380 return
1381
1382 # Run hooks on the basis of whether the files from the gclient operation
1383 # match each hook's pattern.
1384 for hook_dict in hooks:
1385 pattern = re.compile(hook_dict['pattern'])
phajdan.jr@chromium.org71b40682009-07-31 23:40:09 +00001386 matching_file_list = [file for file in file_list if pattern.search(file)]
1387 if matching_file_list:
1388 self._RunHookAction(hook_dict, matching_file_list)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001389
1390 def RunOnDeps(self, command, args):
1391 """Runs a command on each dependency in a client and its dependencies.
1392
1393 The module's dependencies are specified in its top-level DEPS files.
1394
1395 Args:
1396 command: The command to use (e.g., 'status' or 'diff')
1397 args: list of str - extra arguments to add to the command line.
1398
1399 Raises:
1400 Error: If the client has conflicting entries.
1401 """
1402 if not command in self.supported_commands:
1403 raise Error("'%s' is an unsupported command" % command)
1404
1405 # Check for revision overrides.
1406 revision_overrides = {}
1407 for revision in self._options.revisions:
1408 if revision.find("@") == -1:
1409 raise Error(
1410 "Specify the full dependency when specifying a revision number.")
1411 revision_elem = revision.split("@")
1412 # Disallow conflicting revs
1413 if revision_overrides.has_key(revision_elem[0]) and \
1414 revision_overrides[revision_elem[0]] != revision_elem[1]:
1415 raise Error(
1416 "Conflicting revision numbers specified.")
1417 revision_overrides[revision_elem[0]] = revision_elem[1]
1418
1419 solutions = self.GetVar("solutions")
1420 if not solutions:
1421 raise Error("No solution specified")
1422
1423 # When running runhooks --force, there's no need to consult the SCM.
1424 # All known hooks are expected to run unconditionally regardless of working
1425 # copy state, so skip the SCM status check.
1426 run_scm = not (command == 'runhooks' and self._options.force)
1427
1428 entries = {}
1429 entries_deps_content = {}
1430 file_list = []
1431 # Run on the base solutions first.
1432 for solution in solutions:
1433 name = solution["name"]
1434 if name in entries:
1435 raise Error("solution %s specified more than once" % name)
1436 url = solution["url"]
1437 entries[name] = url
1438 if run_scm:
1439 self._options.revision = revision_overrides.get(name)
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001440 scm = SCMWrapper(url, self._root_dir, name)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001441 scm.RunCommand(command, self._options, args, file_list)
phajdan.jr@chromium.orgd83b2b22009-08-11 15:30:55 +00001442 file_list = [os.path.join(name, file.strip()) for file in file_list]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001443 self._options.revision = None
1444 try:
1445 deps_content = FileRead(os.path.join(self._root_dir, name,
1446 self._options.deps_file))
1447 except IOError, e:
1448 if e.errno != errno.ENOENT:
1449 raise
1450 deps_content = ""
1451 entries_deps_content[name] = deps_content
1452
1453 # Process the dependencies next (sort alphanumerically to ensure that
1454 # containing directories get populated first and for readability)
1455 deps = self._ParseAllDeps(entries, entries_deps_content)
1456 deps_to_process = deps.keys()
1457 deps_to_process.sort()
1458
1459 # First pass for direct dependencies.
1460 for d in deps_to_process:
1461 if type(deps[d]) == str:
1462 url = deps[d]
1463 entries[d] = url
1464 if run_scm:
1465 self._options.revision = revision_overrides.get(d)
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001466 scm = SCMWrapper(url, self._root_dir, d)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001467 scm.RunCommand(command, self._options, args, file_list)
1468 self._options.revision = None
1469
1470 # Second pass for inherited deps (via the From keyword)
1471 for d in deps_to_process:
1472 if type(deps[d]) != str:
1473 sub_deps = self._ParseSolutionDeps(
1474 deps[d].module_name,
1475 FileRead(os.path.join(self._root_dir,
1476 deps[d].module_name,
1477 self._options.deps_file)),
1478 {})
1479 url = sub_deps[d]
1480 entries[d] = url
1481 if run_scm:
1482 self._options.revision = revision_overrides.get(d)
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001483 scm = SCMWrapper(url, self._root_dir, d)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001484 scm.RunCommand(command, self._options, args, file_list)
1485 self._options.revision = None
phajdan.jr@chromium.orgd83b2b22009-08-11 15:30:55 +00001486
1487 # Convert all absolute paths to relative.
1488 for i in range(len(file_list)):
1489 # TODO(phajdan.jr): We should know exactly when the paths are absolute.
1490 # It depends on the command being executed (like runhooks vs sync).
1491 if not os.path.isabs(file_list[i]):
1492 continue
1493
1494 prefix = os.path.commonprefix([self._root_dir.lower(),
1495 file_list[i].lower()])
1496 file_list[i] = file_list[i][len(prefix):]
1497
1498 # Strip any leading path separators.
1499 while file_list[i].startswith('\\') or file_list[i].startswith('/'):
1500 file_list[i] = file_list[i][1:]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001501
1502 is_using_git = IsUsingGit(self._root_dir, entries.keys())
1503 self._RunHooks(command, file_list, is_using_git)
1504
1505 if command == 'update':
ajwong@chromium.orgcdcee802009-06-23 15:30:42 +00001506 # Notify the user if there is an orphaned entry in their working copy.
1507 # Only delete the directory if there are no changes in it, and
1508 # delete_unversioned_trees is set to true.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001509 prev_entries = self._ReadEntries()
1510 for entry in prev_entries:
maruel@chromium.orgc5e9aec2009-08-03 18:25:56 +00001511 # Fix path separator on Windows.
1512 entry_fixed = entry.replace('/', os.path.sep)
1513 e_dir = os.path.join(self._root_dir, entry_fixed)
1514 # Use entry and not entry_fixed there.
maruel@chromium.org0329e672009-05-13 18:41:04 +00001515 if entry not in entries and os.path.exists(e_dir):
ajwong@chromium.org8399dc02009-06-23 21:36:25 +00001516 if not self._options.delete_unversioned_trees or \
1517 CaptureSVNStatus(e_dir):
maruel@chromium.orgc5e9aec2009-08-03 18:25:56 +00001518 # There are modified files in this entry. Keep warning until
1519 # removed.
1520 entries[entry] = None
1521 print(("\nWARNING: \"%s\" is no longer part of this client. "
1522 "It is recommended that you manually remove it.\n") %
1523 entry_fixed)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001524 else:
1525 # Delete the entry
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +00001526 print("\n________ deleting \'%s\' " +
maruel@chromium.orgc5e9aec2009-08-03 18:25:56 +00001527 "in \'%s\'") % (entry_fixed, self._root_dir)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001528 RemoveDirectory(e_dir)
1529 # record the current list of entries for next time
1530 self._SaveEntries(entries)
1531
1532 def PrintRevInfo(self):
1533 """Output revision info mapping for the client and its dependencies. This
1534 allows the capture of a overall "revision" for the source tree that can
1535 be used to reproduce the same tree in the future. The actual output
1536 contains enough information (source paths, svn server urls and revisions)
1537 that it can be used either to generate external svn commands (without
1538 gclient) or as input to gclient's --rev option (with some massaging of
1539 the data).
1540
1541 NOTE: Unlike RunOnDeps this does not require a local checkout and is run
1542 on the Pulse master. It MUST NOT execute hooks.
1543
1544 Raises:
1545 Error: If the client has conflicting entries.
1546 """
1547 # Check for revision overrides.
1548 revision_overrides = {}
1549 for revision in self._options.revisions:
1550 if revision.find("@") < 0:
1551 raise Error(
1552 "Specify the full dependency when specifying a revision number.")
1553 revision_elem = revision.split("@")
1554 # Disallow conflicting revs
1555 if revision_overrides.has_key(revision_elem[0]) and \
1556 revision_overrides[revision_elem[0]] != revision_elem[1]:
1557 raise Error(
1558 "Conflicting revision numbers specified.")
1559 revision_overrides[revision_elem[0]] = revision_elem[1]
1560
1561 solutions = self.GetVar("solutions")
1562 if not solutions:
1563 raise Error("No solution specified")
1564
1565 entries = {}
1566 entries_deps_content = {}
1567
1568 # Inner helper to generate base url and rev tuple (including honoring
1569 # |revision_overrides|)
1570 def GetURLAndRev(name, original_url):
1571 if original_url.find("@") < 0:
1572 if revision_overrides.has_key(name):
1573 return (original_url, int(revision_overrides[name]))
1574 else:
1575 # TODO(aharper): SVN/SCMWrapper cleanup (non-local commandset)
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +00001576 return (original_url, CaptureSVNHeadRevision(original_url))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001577 else:
1578 url_components = original_url.split("@")
1579 if revision_overrides.has_key(name):
1580 return (url_components[0], int(revision_overrides[name]))
1581 else:
1582 return (url_components[0], int(url_components[1]))
1583
1584 # Run on the base solutions first.
1585 for solution in solutions:
1586 name = solution["name"]
1587 if name in entries:
1588 raise Error("solution %s specified more than once" % name)
1589 (url, rev) = GetURLAndRev(name, solution["url"])
1590 entries[name] = "%s@%d" % (url, rev)
1591 # TODO(aharper): SVN/SCMWrapper cleanup (non-local commandset)
1592 entries_deps_content[name] = CaptureSVN(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001593 ["cat",
1594 "%s/%s@%d" % (url,
1595 self._options.deps_file,
1596 rev)],
1597 os.getcwd())
1598
1599 # Process the dependencies next (sort alphanumerically to ensure that
1600 # containing directories get populated first and for readability)
1601 deps = self._ParseAllDeps(entries, entries_deps_content)
1602 deps_to_process = deps.keys()
1603 deps_to_process.sort()
1604
1605 # First pass for direct dependencies.
1606 for d in deps_to_process:
1607 if type(deps[d]) == str:
1608 (url, rev) = GetURLAndRev(d, deps[d])
1609 entries[d] = "%s@%d" % (url, rev)
1610
1611 # Second pass for inherited deps (via the From keyword)
1612 for d in deps_to_process:
1613 if type(deps[d]) != str:
1614 deps_parent_url = entries[deps[d].module_name]
1615 if deps_parent_url.find("@") < 0:
1616 raise Error("From %s missing revisioned url" % deps[d].module_name)
1617 deps_parent_url_components = deps_parent_url.split("@")
1618 # TODO(aharper): SVN/SCMWrapper cleanup (non-local commandset)
1619 deps_parent_content = CaptureSVN(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001620 ["cat",
1621 "%s/%s@%s" % (deps_parent_url_components[0],
1622 self._options.deps_file,
1623 deps_parent_url_components[1])],
1624 os.getcwd())
1625 sub_deps = self._ParseSolutionDeps(
1626 deps[d].module_name,
1627 FileRead(os.path.join(self._root_dir,
1628 deps[d].module_name,
1629 self._options.deps_file)),
1630 {})
1631 (url, rev) = GetURLAndRev(d, sub_deps[d])
1632 entries[d] = "%s@%d" % (url, rev)
maruel@chromium.org57e893e2009-08-19 18:12:09 +00001633 print(";\n\n".join(["%s: %s" % (x, entries[x])
1634 for x in sorted(entries.keys())]))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001635
1636
1637## gclient commands.
1638
1639
1640def DoCleanup(options, args):
1641 """Handle the cleanup subcommand.
1642
1643 Raises:
1644 Error: if client isn't configured properly.
1645 """
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001646 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001647 if not client:
1648 raise Error("client not configured; see 'gclient config'")
1649 if options.verbose:
1650 # Print out the .gclient file. This is longer than if we just printed the
1651 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +00001652 print(client.ConfigContent())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001653 options.verbose = True
1654 return client.RunOnDeps('cleanup', args)
1655
1656
1657def DoConfig(options, args):
1658 """Handle the config subcommand.
1659
1660 Args:
1661 options: If options.spec set, a string providing contents of config file.
1662 args: The command line args. If spec is not set,
1663 then args[0] is a string URL to get for config file.
1664
1665 Raises:
1666 Error: on usage error
1667 """
1668 if len(args) < 1 and not options.spec:
1669 raise Error("required argument missing; see 'gclient help config'")
maruel@chromium.org0329e672009-05-13 18:41:04 +00001670 if os.path.exists(options.config_filename):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001671 raise Error("%s file already exists in the current directory" %
1672 options.config_filename)
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001673 client = GClient('.', options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001674 if options.spec:
1675 client.SetConfig(options.spec)
1676 else:
1677 # TODO(darin): it would be nice to be able to specify an alternate relpath
1678 # for the given URL.
maruel@chromium.org1ab7ffc2009-06-03 17:21:37 +00001679 base_url = args[0].rstrip('/')
1680 name = base_url.split("/")[-1]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001681 safesync_url = ""
1682 if len(args) > 1:
1683 safesync_url = args[1]
1684 client.SetDefaultConfig(name, base_url, safesync_url)
1685 client.SaveConfig()
1686
1687
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +00001688def DoExport(options, args):
1689 """Handle the export subcommand.
phajdan.jr@chromium.org71b40682009-07-31 23:40:09 +00001690
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +00001691 Raises:
1692 Error: on usage error
1693 """
1694 if len(args) != 1:
1695 raise Error("Need directory name")
1696 client = GClient.LoadCurrentConfig(options)
1697
1698 if not client:
1699 raise Error("client not configured; see 'gclient config'")
1700
1701 if options.verbose:
1702 # Print out the .gclient file. This is longer than if we just printed the
1703 # client dict, but more legible, and it might contain helpful comments.
1704 print(client.ConfigContent())
1705 return client.RunOnDeps('export', args)
1706
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001707def DoHelp(options, args):
1708 """Handle the help subcommand giving help for another subcommand.
1709
1710 Raises:
1711 Error: if the command is unknown.
1712 """
1713 if len(args) == 1 and args[0] in COMMAND_USAGE_TEXT:
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +00001714 print(COMMAND_USAGE_TEXT[args[0]])
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001715 else:
1716 raise Error("unknown subcommand '%s'; see 'gclient help'" % args[0])
1717
1718
kbr@google.comab318592009-09-04 00:54:55 +00001719def DoPack(options, args):
1720 """Handle the pack subcommand.
1721
1722 Raises:
1723 Error: if client isn't configured properly.
1724 """
1725 client = GClient.LoadCurrentConfig(options)
1726 if not client:
1727 raise Error("client not configured; see 'gclient config'")
1728 if options.verbose:
1729 # Print out the .gclient file. This is longer than if we just printed the
1730 # client dict, but more legible, and it might contain helpful comments.
1731 print(client.ConfigContent())
1732 options.verbose = True
1733 return client.RunOnDeps('pack', args)
1734
1735
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001736def DoStatus(options, args):
1737 """Handle the status subcommand.
1738
1739 Raises:
1740 Error: if client isn't configured properly.
1741 """
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001742 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001743 if not client:
1744 raise Error("client not configured; see 'gclient config'")
1745 if options.verbose:
1746 # Print out the .gclient file. This is longer than if we just printed the
1747 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +00001748 print(client.ConfigContent())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001749 options.verbose = True
1750 return client.RunOnDeps('status', args)
1751
1752
1753def DoUpdate(options, args):
1754 """Handle the update and sync subcommands.
1755
1756 Raises:
1757 Error: if client isn't configured properly.
1758 """
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001759 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001760
1761 if not client:
1762 raise Error("client not configured; see 'gclient config'")
1763
1764 if not options.head:
1765 solutions = client.GetVar('solutions')
1766 if solutions:
1767 for s in solutions:
1768 if s.get('safesync_url', ''):
1769 # rip through revisions and make sure we're not over-riding
1770 # something that was explicitly passed
1771 has_key = False
1772 for r in options.revisions:
1773 if r.split('@')[0] == s['name']:
1774 has_key = True
1775 break
1776
1777 if not has_key:
1778 handle = urllib.urlopen(s['safesync_url'])
1779 rev = handle.read().strip()
1780 handle.close()
1781 if len(rev):
1782 options.revisions.append(s['name']+'@'+rev)
1783
1784 if options.verbose:
1785 # Print out the .gclient file. This is longer than if we just printed the
1786 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +00001787 print(client.ConfigContent())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001788 return client.RunOnDeps('update', args)
1789
1790
1791def DoDiff(options, args):
1792 """Handle the diff subcommand.
1793
1794 Raises:
1795 Error: if client isn't configured properly.
1796 """
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001797 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001798 if not client:
1799 raise Error("client not configured; see 'gclient config'")
1800 if options.verbose:
1801 # Print out the .gclient file. This is longer than if we just printed the
1802 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +00001803 print(client.ConfigContent())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001804 options.verbose = True
1805 return client.RunOnDeps('diff', args)
1806
1807
1808def DoRevert(options, args):
1809 """Handle the revert subcommand.
1810
1811 Raises:
1812 Error: if client isn't configured properly.
1813 """
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001814 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001815 if not client:
1816 raise Error("client not configured; see 'gclient config'")
1817 return client.RunOnDeps('revert', args)
1818
1819
1820def DoRunHooks(options, args):
1821 """Handle the runhooks subcommand.
1822
1823 Raises:
1824 Error: if client isn't configured properly.
1825 """
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001826 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001827 if not client:
1828 raise Error("client not configured; see 'gclient config'")
1829 if options.verbose:
1830 # Print out the .gclient file. This is longer than if we just printed the
1831 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +00001832 print(client.ConfigContent())
maruel@chromium.org5df6a462009-08-28 18:52:26 +00001833 options.force = True
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001834 return client.RunOnDeps('runhooks', args)
1835
1836
1837def DoRevInfo(options, args):
1838 """Handle the revinfo subcommand.
1839
1840 Raises:
1841 Error: if client isn't configured properly.
1842 """
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001843 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001844 if not client:
1845 raise Error("client not configured; see 'gclient config'")
1846 client.PrintRevInfo()
1847
1848
1849gclient_command_map = {
1850 "cleanup": DoCleanup,
1851 "config": DoConfig,
1852 "diff": DoDiff,
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +00001853 "export": DoExport,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001854 "help": DoHelp,
kbr@google.comab318592009-09-04 00:54:55 +00001855 "pack": DoPack,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001856 "status": DoStatus,
1857 "sync": DoUpdate,
1858 "update": DoUpdate,
1859 "revert": DoRevert,
1860 "runhooks": DoRunHooks,
1861 "revinfo" : DoRevInfo,
1862}
1863
1864
1865def DispatchCommand(command, options, args, command_map=None):
1866 """Dispatches the appropriate subcommand based on command line arguments."""
1867 if command_map is None:
1868 command_map = gclient_command_map
1869
1870 if command in command_map:
1871 return command_map[command](options, args)
1872 else:
1873 raise Error("unknown subcommand '%s'; see 'gclient help'" % command)
1874
1875
1876def Main(argv):
1877 """Parse command line arguments and dispatch command."""
1878
1879 option_parser = optparse.OptionParser(usage=DEFAULT_USAGE_TEXT,
1880 version=__version__)
1881 option_parser.disable_interspersed_args()
1882 option_parser.add_option("", "--force", action="store_true", default=False,
1883 help=("(update/sync only) force update even "
1884 "for modules which haven't changed"))
evan@chromium.org67820ef2009-07-27 17:23:00 +00001885 option_parser.add_option("", "--nohooks", action="store_true", default=False,
1886 help=("(update/sync/revert only) prevent the hooks from "
1887 "running"))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001888 option_parser.add_option("", "--revision", action="append", dest="revisions",
1889 metavar="REV", default=[],
1890 help=("(update/sync only) sync to a specific "
1891 "revision, can be used multiple times for "
1892 "each solution, e.g. --revision=src@123, "
1893 "--revision=internal@32"))
1894 option_parser.add_option("", "--deps", default=None, dest="deps_os",
1895 metavar="OS_LIST",
1896 help=("(update/sync only) sync deps for the "
1897 "specified (comma-separated) platform(s); "
1898 "'all' will sync all platforms"))
1899 option_parser.add_option("", "--spec", default=None,
1900 help=("(config only) create a gclient file "
1901 "containing the provided string"))
1902 option_parser.add_option("", "--verbose", action="store_true", default=False,
1903 help="produce additional output for diagnostics")
1904 option_parser.add_option("", "--manually_grab_svn_rev", action="store_true",
1905 default=False,
1906 help="Skip svn up whenever possible by requesting "
1907 "actual HEAD revision from the repository")
1908 option_parser.add_option("", "--head", action="store_true", default=False,
1909 help=("skips any safesync_urls specified in "
1910 "configured solutions"))
ajwong@chromium.orgcdcee802009-06-23 15:30:42 +00001911 option_parser.add_option("", "--delete_unversioned_trees",
1912 action="store_true", default=False,
1913 help=("on update, delete any unexpected "
1914 "unversioned trees that are in the checkout"))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001915
1916 if len(argv) < 2:
1917 # Users don't need to be told to use the 'help' command.
1918 option_parser.print_help()
1919 return 1
1920 # Add manual support for --version as first argument.
1921 if argv[1] == '--version':
1922 option_parser.print_version()
1923 return 0
1924
1925 # Add manual support for --help as first argument.
1926 if argv[1] == '--help':
1927 argv[1] = 'help'
1928
1929 command = argv[1]
1930 options, args = option_parser.parse_args(argv[2:])
1931
1932 if len(argv) < 3 and command == "help":
1933 option_parser.print_help()
1934 return 0
1935
1936 # Files used for configuration and state saving.
1937 options.config_filename = os.environ.get("GCLIENT_FILE", ".gclient")
1938 options.entries_filename = ".gclient_entries"
1939 options.deps_file = "DEPS"
1940
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001941 options.platform = sys.platform
1942 return DispatchCommand(command, options, args)
1943
1944
1945if "__main__" == __name__:
1946 try:
1947 result = Main(sys.argv)
1948 except Error, e:
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +00001949 print >> sys.stderr, "Error: %s" % str(e)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001950 result = 1
1951 sys.exit(result)
1952
1953# vim: ts=2:sw=2:tw=80:et: