blob: 743557c411e37bbaf6cb480bc1a078eb50e4fdef [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
gspencer@google.comdf2d5902009-09-11 22:16:21 +0000265DEFAULT_CLIENT_FILE_TEXT = ("""\
266# An element of this array (a "solution") describes a repository directory
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000267# that will be checked out into your working copy. Each solution may
268# optionally define additional dependencies (via its DEPS file) to be
269# checked out alongside the solution's directory. A solution may also
gspencer@google.comdf2d5902009-09-11 22:16:21 +0000270# specify custom dependencies (via the "custom_deps" property) that
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000271# override or augment the dependencies specified by the DEPS file.
gspencer@google.comdf2d5902009-09-11 22:16:21 +0000272# If a "safesync_url" is specified, it is assumed to reference the location of
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000273# a text file which contains nothing but the last known good SCM revision to
274# sync against. It is fetched if specified and used unless --head is passed
gspencer@google.comdf2d5902009-09-11 22:16:21 +0000275
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000276solutions = [
gspencer@google.comdf2d5902009-09-11 22:16:21 +0000277 { "name" : "%(solution_name)s",
278 "url" : "%(solution_url)s",
279 "custom_deps" : {
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000280 # To use the trunk of a component instead of what's in DEPS:
gspencer@google.comdf2d5902009-09-11 22:16:21 +0000281 #"component": "https://svnserver/component/trunk/",
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000282 # To exclude a component from your working copy:
gspencer@google.comdf2d5902009-09-11 22:16:21 +0000283 #"data/really_large_component": None,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000284 },
gspencer@google.comdf2d5902009-09-11 22:16:21 +0000285 "safesync_url": "%(safesync_url)s"
286 },
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000287]
288""")
289
290
291## Generic utils
292
maruel@chromium.orge105d8d2009-04-30 17:58:25 +0000293def ParseXML(output):
294 try:
295 return xml.dom.minidom.parseString(output)
296 except xml.parsers.expat.ExpatError:
297 return None
298
299
maruel@chromium.org483b0082009-05-07 02:57:14 +0000300def GetNamedNodeText(node, node_name):
301 child_nodes = node.getElementsByTagName(node_name)
302 if not child_nodes:
303 return None
304 assert len(child_nodes) == 1 and child_nodes[0].childNodes.length == 1
305 return child_nodes[0].firstChild.nodeValue
306
307
308def GetNodeNamedAttributeText(node, node_name, attribute_name):
309 child_nodes = node.getElementsByTagName(node_name)
310 if not child_nodes:
311 return None
312 assert len(child_nodes) == 1
313 return child_nodes[0].getAttribute(attribute_name)
314
315
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000316class Error(Exception):
317 """gclient exception class."""
318 pass
319
320class PrintableObject(object):
321 def __str__(self):
322 output = ''
323 for i in dir(self):
324 if i.startswith('__'):
325 continue
326 output += '%s = %s\n' % (i, str(getattr(self, i, '')))
327 return output
328
329
330def FileRead(filename):
331 content = None
332 f = open(filename, "rU")
333 try:
334 content = f.read()
335 finally:
336 f.close()
337 return content
338
339
340def FileWrite(filename, content):
341 f = open(filename, "w")
342 try:
343 f.write(content)
344 finally:
345 f.close()
346
347
348def RemoveDirectory(*path):
349 """Recursively removes a directory, even if it's marked read-only.
350
351 Remove the directory located at *path, if it exists.
352
353 shutil.rmtree() doesn't work on Windows if any of the files or directories
354 are read-only, which svn repositories and some .svn files are. We need to
355 be able to force the files to be writable (i.e., deletable) as we traverse
356 the tree.
357
358 Even with all this, Windows still sometimes fails to delete a file, citing
359 a permission error (maybe something to do with antivirus scans or disk
360 indexing). The best suggestion any of the user forums had was to wait a
361 bit and try again, so we do that too. It's hand-waving, but sometimes it
362 works. :/
363
364 On POSIX systems, things are a little bit simpler. The modes of the files
365 to be deleted doesn't matter, only the modes of the directories containing
366 them are significant. As the directory tree is traversed, each directory
367 has its mode set appropriately before descending into it. This should
368 result in the entire tree being removed, with the possible exception of
369 *path itself, because nothing attempts to change the mode of its parent.
370 Doing so would be hazardous, as it's not a directory slated for removal.
371 In the ordinary case, this is not a problem: for our purposes, the user
372 will never lack write permission on *path's parent.
373 """
374 file_path = os.path.join(*path)
375 if not os.path.exists(file_path):
376 return
377
378 if os.path.islink(file_path) or not os.path.isdir(file_path):
379 raise Error("RemoveDirectory asked to remove non-directory %s" % file_path)
380
381 has_win32api = False
382 if sys.platform == 'win32':
383 has_win32api = True
384 # Some people don't have the APIs installed. In that case we'll do without.
385 try:
386 win32api = __import__('win32api')
387 win32con = __import__('win32con')
388 except ImportError:
389 has_win32api = False
390 else:
391 # On POSIX systems, we need the x-bit set on the directory to access it,
392 # the r-bit to see its contents, and the w-bit to remove files from it.
393 # The actual modes of the files within the directory is irrelevant.
394 os.chmod(file_path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
395 for fn in os.listdir(file_path):
396 fullpath = os.path.join(file_path, fn)
397
398 # If fullpath is a symbolic link that points to a directory, isdir will
399 # be True, but we don't want to descend into that as a directory, we just
400 # want to remove the link. Check islink and treat links as ordinary files
401 # would be treated regardless of what they reference.
402 if os.path.islink(fullpath) or not os.path.isdir(fullpath):
403 if sys.platform == 'win32':
404 os.chmod(fullpath, stat.S_IWRITE)
405 if has_win32api:
406 win32api.SetFileAttributes(fullpath, win32con.FILE_ATTRIBUTE_NORMAL)
407 try:
408 os.remove(fullpath)
409 except OSError, e:
410 if e.errno != errno.EACCES or sys.platform != 'win32':
411 raise
412 print 'Failed to delete %s: trying again' % fullpath
413 time.sleep(0.1)
414 os.remove(fullpath)
415 else:
416 RemoveDirectory(fullpath)
417
418 if sys.platform == 'win32':
419 os.chmod(file_path, stat.S_IWRITE)
420 if has_win32api:
421 win32api.SetFileAttributes(file_path, win32con.FILE_ATTRIBUTE_NORMAL)
422 try:
423 os.rmdir(file_path)
424 except OSError, e:
425 if e.errno != errno.EACCES or sys.platform != 'win32':
426 raise
427 print 'Failed to remove %s: trying again' % file_path
428 time.sleep(0.1)
429 os.rmdir(file_path)
430
431
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000432def SubprocessCall(command, in_directory, fail_status=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000433 """Runs command, a list, in directory in_directory.
434
kbr@google.comab318592009-09-04 00:54:55 +0000435 This function wraps SubprocessCallAndFilter, but does not perform the
436 filtering functions. See that function for a more complete usage
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000437 description.
438 """
439 # Call subprocess and capture nothing:
kbr@google.comab318592009-09-04 00:54:55 +0000440 SubprocessCallAndFilter(command, in_directory, True, True, fail_status)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000441
442
kbr@google.comab318592009-09-04 00:54:55 +0000443def SubprocessCallAndFilter(command,
444 in_directory,
445 print_messages,
446 print_stdout,
447 fail_status=None, filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000448 """Runs command, a list, in directory in_directory.
449
kbr@google.comab318592009-09-04 00:54:55 +0000450 If print_messages is true, a message indicating what is being done
451 is printed to stdout. If print_stdout is true, the command's stdout
452 is also forwarded to stdout.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000453
kbr@google.comab318592009-09-04 00:54:55 +0000454 If a filter function is specified, it is expected to take a single
455 string argument, and it will be called with each line of the
456 subprocess's output. Each line has had the trailing newline character
457 trimmed.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000458
459 If the command fails, as indicated by a nonzero exit status, gclient will
460 exit with an exit status of fail_status. If fail_status is None (the
461 default), gclient will raise an Error exception.
462 """
463
kbr@google.comab318592009-09-04 00:54:55 +0000464 if print_messages:
465 print("\n________ running \'%s\' in \'%s\'"
466 % (' '.join(command), in_directory))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000467
468 # *Sigh*: Windows needs shell=True, or else it won't search %PATH% for the
469 # executable, but shell=True makes subprocess on Linux fail when it's called
470 # with a list because it only tries to execute the first item in the list.
471 kid = subprocess.Popen(command, bufsize=0, cwd=in_directory,
472 shell=(sys.platform == 'win32'), stdout=subprocess.PIPE)
473
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000474 # Also, we need to forward stdout to prevent weird re-ordering of output.
475 # This has to be done on a per byte basis to make sure it is not buffered:
476 # normally buffering is done for each line, but if svn requests input, no
477 # end-of-line character is output after the prompt and it would not show up.
478 in_byte = kid.stdout.read(1)
479 in_line = ""
480 while in_byte:
481 if in_byte != "\r":
kbr@google.comab318592009-09-04 00:54:55 +0000482 if print_stdout:
483 sys.stdout.write(in_byte)
484 if in_byte != "\n":
485 in_line += in_byte
486 if in_byte == "\n" and filter:
487 filter(in_line)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000488 in_line = ""
489 in_byte = kid.stdout.read(1)
490 rv = kid.wait()
491
492 if rv:
493 msg = "failed to run command: %s" % " ".join(command)
494
495 if fail_status != None:
496 print >>sys.stderr, msg
497 sys.exit(fail_status)
498
499 raise Error(msg)
500
501
502def IsUsingGit(root, paths):
503 """Returns True if we're using git to manage any of our checkouts.
504 |entries| is a list of paths to check."""
505 for path in paths:
506 if os.path.exists(os.path.join(root, path, '.git')):
507 return True
508 return False
509
510# -----------------------------------------------------------------------------
511# SVN utils:
512
513
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000514def RunSVN(args, in_directory):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000515 """Runs svn, sending output to stdout.
516
517 Args:
518 args: A sequence of command line parameters to be passed to svn.
519 in_directory: The directory where svn is to be run.
520
521 Raises:
522 Error: An error occurred while running the svn command.
523 """
524 c = [SVN_COMMAND]
525 c.extend(args)
526
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000527 SubprocessCall(c, in_directory)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000528
529
maruel@chromium.org5c3a2ff2009-05-12 19:28:55 +0000530def CaptureSVN(args, in_directory=None, print_error=True):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000531 """Runs svn, capturing output sent to stdout as a string.
532
533 Args:
534 args: A sequence of command line parameters to be passed to svn.
535 in_directory: The directory where svn is to be run.
536
537 Returns:
538 The output sent to stdout as a string.
539 """
540 c = [SVN_COMMAND]
541 c.extend(args)
542
543 # *Sigh*: Windows needs shell=True, or else it won't search %PATH% for
544 # the svn.exe executable, but shell=True makes subprocess on Linux fail
545 # when it's called with a list because it only tries to execute the
546 # first string ("svn").
maruel@chromium.org5c3a2ff2009-05-12 19:28:55 +0000547 stderr = None
maruel@chromium.org672343d2009-05-20 20:03:25 +0000548 if not print_error:
maruel@chromium.org5c3a2ff2009-05-12 19:28:55 +0000549 stderr = subprocess.PIPE
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000550 return subprocess.Popen(c,
551 cwd=in_directory,
552 shell=(sys.platform == 'win32'),
maruel@chromium.org5c3a2ff2009-05-12 19:28:55 +0000553 stdout=subprocess.PIPE,
554 stderr=stderr).communicate()[0]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000555
556
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000557def RunSVNAndGetFileList(args, in_directory, file_list):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000558 """Runs svn checkout, update, or status, output to stdout.
559
560 The first item in args must be either "checkout", "update", or "status".
561
562 svn's stdout is parsed to collect a list of files checked out or updated.
563 These files are appended to file_list. svn's stdout is also printed to
564 sys.stdout as in RunSVN.
565
566 Args:
567 args: A sequence of command line parameters to be passed to svn.
568 in_directory: The directory where svn is to be run.
569
570 Raises:
571 Error: An error occurred while running the svn command.
572 """
573 command = [SVN_COMMAND]
574 command.extend(args)
575
576 # svn update and svn checkout use the same pattern: the first three columns
577 # are for file status, property status, and lock status. This is followed
578 # by two spaces, and then the path to the file.
579 update_pattern = '^... (.*)$'
580
581 # The first three columns of svn status are the same as for svn update and
582 # svn checkout. The next three columns indicate addition-with-history,
583 # switch, and remote lock status. This is followed by one space, and then
584 # the path to the file.
585 status_pattern = '^...... (.*)$'
586
587 # args[0] must be a supported command. This will blow up if it's something
588 # else, which is good. Note that the patterns are only effective when
589 # these commands are used in their ordinary forms, the patterns are invalid
590 # for "svn status --show-updates", for example.
591 pattern = {
592 'checkout': update_pattern,
593 'status': status_pattern,
594 'update': update_pattern,
595 }[args[0]]
596
kbr@google.comab318592009-09-04 00:54:55 +0000597 compiled_pattern = re.compile(pattern)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000598
kbr@google.comab318592009-09-04 00:54:55 +0000599 def CaptureMatchingLines(line):
600 match = compiled_pattern.search(line)
601 if match:
602 file_list.append(match.group(1))
603
604 RunSVNAndFilterOutput(args,
605 in_directory,
606 True,
607 True,
608 CaptureMatchingLines)
609
610def RunSVNAndFilterOutput(args,
611 in_directory,
612 print_messages,
613 print_stdout,
614 filter):
615 """Runs svn checkout, update, status, or diff, optionally outputting
616 to stdout.
617
618 The first item in args must be either "checkout", "update",
619 "status", or "diff".
620
621 svn's stdout is passed line-by-line to the given filter function. If
622 print_stdout is true, it is also printed to sys.stdout as in RunSVN.
623
624 Args:
625 args: A sequence of command line parameters to be passed to svn.
626 in_directory: The directory where svn is to be run.
627 print_messages: Whether to print status messages to stdout about
628 which Subversion commands are being run.
629 print_stdout: Whether to forward Subversion's output to stdout.
630 filter: A function taking one argument (a string) which will be
631 passed each line (with the ending newline character removed) of
632 Subversion's output for filtering.
633
634 Raises:
635 Error: An error occurred while running the svn command.
636 """
637 command = [SVN_COMMAND]
638 command.extend(args)
639
640 SubprocessCallAndFilter(command,
641 in_directory,
642 print_messages,
643 print_stdout,
644 filter=filter)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000645
maruel@chromium.org5c3a2ff2009-05-12 19:28:55 +0000646def CaptureSVNInfo(relpath, in_directory=None, print_error=True):
maruel@chromium.org2dc8a4d2009-05-11 12:41:20 +0000647 """Returns a dictionary from the svn info output for the given file.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000648
649 Args:
650 relpath: The directory where the working copy resides relative to
651 the directory given by in_directory.
652 in_directory: The directory where svn is to be run.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000653 """
maruel@chromium.org5c3a2ff2009-05-12 19:28:55 +0000654 output = CaptureSVN(["info", "--xml", relpath], in_directory, print_error)
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000655 dom = ParseXML(output)
maruel@chromium.org2dc8a4d2009-05-11 12:41:20 +0000656 result = {}
maruel@chromium.org483b0082009-05-07 02:57:14 +0000657 if dom:
maruel@chromium.org2dc8a4d2009-05-11 12:41:20 +0000658 def C(item, f):
659 if item is not None: return f(item)
maruel@chromium.org483b0082009-05-07 02:57:14 +0000660 # /info/entry/
661 # url
662 # reposityory/(root|uuid)
663 # wc-info/(schedule|depth)
664 # commit/(author|date)
665 # str() the results because they may be returned as Unicode, which
666 # interferes with the higher layers matching up things in the deps
667 # dictionary.
maruel@chromium.org2dc8a4d2009-05-11 12:41:20 +0000668 # TODO(maruel): Fix at higher level instead (!)
669 result['Repository Root'] = C(GetNamedNodeText(dom, 'root'), str)
670 result['URL'] = C(GetNamedNodeText(dom, 'url'), str)
671 result['UUID'] = C(GetNamedNodeText(dom, 'uuid'), str)
672 result['Revision'] = C(GetNodeNamedAttributeText(dom, 'entry', 'revision'),
673 int)
674 result['Node Kind'] = C(GetNodeNamedAttributeText(dom, 'entry', 'kind'),
675 str)
676 result['Schedule'] = C(GetNamedNodeText(dom, 'schedule'), str)
677 result['Path'] = C(GetNodeNamedAttributeText(dom, 'entry', 'path'), str)
678 result['Copied From URL'] = C(GetNamedNodeText(dom, 'copy-from-url'), str)
679 result['Copied From Rev'] = C(GetNamedNodeText(dom, 'copy-from-rev'), str)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000680 return result
681
682
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000683def CaptureSVNHeadRevision(url):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000684 """Get the head revision of a SVN repository.
685
686 Returns:
687 Int head revision
688 """
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000689 info = CaptureSVN(["info", "--xml", url], os.getcwd())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000690 dom = xml.dom.minidom.parseString(info)
691 return int(dom.getElementsByTagName('entry')[0].getAttribute('revision'))
692
693
maruel@chromium.org4810a962009-05-12 21:03:34 +0000694def CaptureSVNStatus(files):
695 """Returns the svn 1.5 svn status emulated output.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000696
maruel@chromium.org4810a962009-05-12 21:03:34 +0000697 @files can be a string (one file) or a list of files.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000698
maruel@chromium.org4810a962009-05-12 21:03:34 +0000699 Returns an array of (status, file) tuples."""
700 command = ["status", "--xml"]
701 if not files:
702 pass
703 elif isinstance(files, basestring):
704 command.append(files)
705 else:
706 command.extend(files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000707
maruel@chromium.org4810a962009-05-12 21:03:34 +0000708 status_letter = {
709 None: ' ',
710 '': ' ',
711 'added': 'A',
712 'conflicted': 'C',
713 'deleted': 'D',
714 'external': 'X',
715 'ignored': 'I',
716 'incomplete': '!',
717 'merged': 'G',
718 'missing': '!',
719 'modified': 'M',
720 'none': ' ',
721 'normal': ' ',
722 'obstructed': '~',
723 'replaced': 'R',
724 'unversioned': '?',
725 }
726 dom = ParseXML(CaptureSVN(command))
maruel@chromium.orge105d8d2009-04-30 17:58:25 +0000727 results = []
728 if dom:
729 # /status/target/entry/(wc-status|commit|author|date)
730 for target in dom.getElementsByTagName('target'):
731 base_path = target.getAttribute('path')
732 for entry in target.getElementsByTagName('entry'):
733 file = entry.getAttribute('path')
734 wc_status = entry.getElementsByTagName('wc-status')
735 assert len(wc_status) == 1
736 # Emulate svn 1.5 status ouput...
737 statuses = [' ' for i in range(7)]
738 # Col 0
739 xml_item_status = wc_status[0].getAttribute('item')
maruel@chromium.org4810a962009-05-12 21:03:34 +0000740 if xml_item_status in status_letter:
741 statuses[0] = status_letter[xml_item_status]
maruel@chromium.orge105d8d2009-04-30 17:58:25 +0000742 else:
743 raise Exception('Unknown item status "%s"; please implement me!' %
744 xml_item_status)
745 # Col 1
746 xml_props_status = wc_status[0].getAttribute('props')
747 if xml_props_status == 'modified':
748 statuses[1] = 'M'
749 elif xml_props_status == 'conflicted':
750 statuses[1] = 'C'
751 elif (not xml_props_status or xml_props_status == 'none' or
752 xml_props_status == 'normal'):
753 pass
754 else:
755 raise Exception('Unknown props status "%s"; please implement me!' %
756 xml_props_status)
maruel@chromium.orgedd27d12009-05-01 17:46:56 +0000757 # Col 2
758 if wc_status[0].getAttribute('wc-locked') == 'true':
759 statuses[2] = 'L'
maruel@chromium.orge105d8d2009-04-30 17:58:25 +0000760 # Col 3
761 if wc_status[0].getAttribute('copied') == 'true':
762 statuses[3] = '+'
maruel@chromium.org4810a962009-05-12 21:03:34 +0000763 item = (''.join(statuses), file)
maruel@chromium.orge105d8d2009-04-30 17:58:25 +0000764 results.append(item)
765 return results
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000766
767
768### SCM abstraction layer
769
770
771class SCMWrapper(object):
772 """Add necessary glue between all the supported SCM.
773
774 This is the abstraction layer to bind to different SCM. Since currently only
775 subversion is supported, a lot of subersionism remains. This can be sorted out
776 once another SCM is supported."""
777 def __init__(self, url=None, root_dir=None, relpath=None,
778 scm_name='svn'):
779 # TODO(maruel): Deduce the SCM from the url.
780 self.scm_name = scm_name
781 self.url = url
782 self._root_dir = root_dir
783 if self._root_dir:
maruel@chromium.orge105d8d2009-04-30 17:58:25 +0000784 self._root_dir = self._root_dir.replace('/', os.sep)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000785 self.relpath = relpath
786 if self.relpath:
maruel@chromium.orge105d8d2009-04-30 17:58:25 +0000787 self.relpath = self.relpath.replace('/', os.sep)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000788
789 def FullUrlForRelativeUrl(self, url):
790 # Find the forth '/' and strip from there. A bit hackish.
791 return '/'.join(self.url.split('/')[:4]) + url
792
793 def RunCommand(self, command, options, args, file_list=None):
794 # file_list will have all files that are modified appended to it.
795
796 if file_list == None:
797 file_list = []
798
799 commands = {
800 'cleanup': self.cleanup,
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +0000801 'export': self.export,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000802 'update': self.update,
803 'revert': self.revert,
804 'status': self.status,
805 'diff': self.diff,
kbr@google.comab318592009-09-04 00:54:55 +0000806 'pack': self.pack,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000807 'runhooks': self.status,
808 }
809
810 if not command in commands:
811 raise Error('Unknown command %s' % command)
812
813 return commands[command](options, args, file_list)
814
815 def cleanup(self, options, args, file_list):
816 """Cleanup working copy."""
817 command = ['cleanup']
818 command.extend(args)
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000819 RunSVN(command, os.path.join(self._root_dir, self.relpath))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000820
821 def diff(self, options, args, file_list):
822 # NOTE: This function does not currently modify file_list.
823 command = ['diff']
824 command.extend(args)
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000825 RunSVN(command, os.path.join(self._root_dir, self.relpath))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000826
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +0000827 def export(self, options, args, file_list):
828 assert len(args) == 1
829 export_path = os.path.abspath(os.path.join(args[0], self.relpath))
830 try:
831 os.makedirs(export_path)
832 except OSError:
833 pass
834 assert os.path.exists(export_path)
835 command = ['export', '--force', '.']
836 command.append(export_path)
837 RunSVN(command, os.path.join(self._root_dir, self.relpath))
838
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000839 def update(self, options, args, file_list):
840 """Runs SCM to update or transparently checkout the working copy.
841
842 All updated files will be appended to file_list.
843
844 Raises:
845 Error: if can't get URL for relative path.
846 """
847 # Only update if git is not controlling the directory.
maruel@chromium.org8626ff72009-05-13 02:57:02 +0000848 checkout_path = os.path.join(self._root_dir, self.relpath)
maruel@chromium.org0329e672009-05-13 18:41:04 +0000849 git_path = os.path.join(self._root_dir, self.relpath, '.git')
850 if os.path.exists(git_path):
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000851 print("________ found .git directory; skipping %s" % self.relpath)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000852 return
853
854 if args:
855 raise Error("Unsupported argument(s): %s" % ",".join(args))
856
857 url = self.url
858 components = url.split("@")
859 revision = None
860 forced_revision = False
861 if options.revision:
862 # Override the revision number.
863 url = '%s@%s' % (components[0], str(options.revision))
864 revision = int(options.revision)
865 forced_revision = True
866 elif len(components) == 2:
867 revision = int(components[1])
868 forced_revision = True
869
870 rev_str = ""
871 if revision:
872 rev_str = ' at %d' % revision
873
maruel@chromium.org0329e672009-05-13 18:41:04 +0000874 if not os.path.exists(checkout_path):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000875 # We need to checkout.
maruel@chromium.org8626ff72009-05-13 02:57:02 +0000876 command = ['checkout', url, checkout_path]
maruel@chromium.orgedd27d12009-05-01 17:46:56 +0000877 if revision:
878 command.extend(['--revision', str(revision)])
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000879 RunSVNAndGetFileList(command, self._root_dir, file_list)
maruel@chromium.orgedd27d12009-05-01 17:46:56 +0000880 return
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000881
882 # Get the existing scm url and the revision number of the current checkout.
maruel@chromium.org8626ff72009-05-13 02:57:02 +0000883 from_info = CaptureSVNInfo(os.path.join(checkout_path, '.'), '.')
maruel@chromium.org1998c6d2009-05-15 12:38:12 +0000884 if not from_info:
885 raise Error("Can't update/checkout %r if an unversioned directory is "
886 "present. Delete the directory and try again." %
887 checkout_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000888
889 if options.manually_grab_svn_rev:
890 # Retrieve the current HEAD version because svn is slow at null updates.
891 if not revision:
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000892 from_info_live = CaptureSVNInfo(from_info['URL'], '.')
maruel@chromium.org2dc8a4d2009-05-11 12:41:20 +0000893 revision = int(from_info_live['Revision'])
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000894 rev_str = ' at %d' % revision
895
maruel@chromium.org2dc8a4d2009-05-11 12:41:20 +0000896 if from_info['URL'] != components[0]:
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000897 to_info = CaptureSVNInfo(url, '.')
maruel@chromium.org8626ff72009-05-13 02:57:02 +0000898 can_switch = ((from_info['Repository Root'] != to_info['Repository Root'])
899 and (from_info['UUID'] == to_info['UUID']))
900 if can_switch:
901 print("\n_____ relocating %s to a new checkout" % self.relpath)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000902 # We have different roots, so check if we can switch --relocate.
903 # Subversion only permits this if the repository UUIDs match.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000904 # Perform the switch --relocate, then rewrite the from_url
905 # to reflect where we "are now." (This is the same way that
906 # Subversion itself handles the metadata when switch --relocate
907 # is used.) This makes the checks below for whether we
908 # can update to a revision or have to switch to a different
909 # branch work as expected.
910 # TODO(maruel): TEST ME !
maruel@chromium.org2dc8a4d2009-05-11 12:41:20 +0000911 command = ["switch", "--relocate",
912 from_info['Repository Root'],
913 to_info['Repository Root'],
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000914 self.relpath]
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000915 RunSVN(command, self._root_dir)
maruel@chromium.org2dc8a4d2009-05-11 12:41:20 +0000916 from_info['URL'] = from_info['URL'].replace(
917 from_info['Repository Root'],
918 to_info['Repository Root'])
maruel@chromium.org8626ff72009-05-13 02:57:02 +0000919 else:
920 if CaptureSVNStatus(checkout_path):
921 raise Error("Can't switch the checkout to %s; UUID don't match and "
922 "there is local changes in %s. Delete the directory and "
923 "try again." % (url, checkout_path))
924 # Ok delete it.
925 print("\n_____ switching %s to a new checkout" % self.relpath)
926 RemoveDirectory(checkout_path)
927 # We need to checkout.
928 command = ['checkout', url, checkout_path]
929 if revision:
930 command.extend(['--revision', str(revision)])
931 RunSVNAndGetFileList(command, self._root_dir, file_list)
932 return
phajdan.jr@chromium.org71b40682009-07-31 23:40:09 +0000933
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000934
935 # If the provided url has a revision number that matches the revision
936 # number of the existing directory, then we don't need to bother updating.
maruel@chromium.org2dc8a4d2009-05-11 12:41:20 +0000937 if not options.force and from_info['Revision'] == revision:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000938 if options.verbose or not forced_revision:
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000939 print("\n_____ %s%s" % (self.relpath, rev_str))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000940 return
941
maruel@chromium.org8626ff72009-05-13 02:57:02 +0000942 command = ["update", checkout_path]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000943 if revision:
944 command.extend(['--revision', str(revision)])
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000945 RunSVNAndGetFileList(command, self._root_dir, file_list)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000946
947 def revert(self, options, args, file_list):
948 """Reverts local modifications. Subversion specific.
949
950 All reverted files will be appended to file_list, even if Subversion
951 doesn't know about them.
952 """
953 path = os.path.join(self._root_dir, self.relpath)
954 if not os.path.isdir(path):
maruel@chromium.orgedd27d12009-05-01 17:46:56 +0000955 # svn revert won't work if the directory doesn't exist. It needs to
956 # checkout instead.
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000957 print("\n_____ %s is missing, synching instead" % self.relpath)
maruel@chromium.orgedd27d12009-05-01 17:46:56 +0000958 # Don't reuse the args.
959 return self.update(options, [], file_list)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000960
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000961 files = CaptureSVNStatus(path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000962 # Batch the command.
963 files_to_revert = []
964 for file in files:
maruel@chromium.org4810a962009-05-12 21:03:34 +0000965 file_path = os.path.join(path, file[1])
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000966 print(file_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000967 # Unversioned file or unexpected unversioned file.
maruel@chromium.org4810a962009-05-12 21:03:34 +0000968 if file[0][0] in ('?', '~'):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000969 # Remove extraneous file. Also remove unexpected unversioned
970 # directories. svn won't touch them but we want to delete these.
971 file_list.append(file_path)
972 try:
973 os.remove(file_path)
974 except EnvironmentError:
975 RemoveDirectory(file_path)
976
maruel@chromium.org4810a962009-05-12 21:03:34 +0000977 if file[0][0] != '?':
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000978 # For any other status, svn revert will work.
979 file_list.append(file_path)
maruel@chromium.org4810a962009-05-12 21:03:34 +0000980 files_to_revert.append(file[1])
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000981
982 # Revert them all at once.
983 if files_to_revert:
984 accumulated_paths = []
985 accumulated_length = 0
986 command = ['revert']
987 for p in files_to_revert:
988 # Some shell have issues with command lines too long.
989 if accumulated_length and accumulated_length + len(p) > 3072:
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000990 RunSVN(command + accumulated_paths,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000991 os.path.join(self._root_dir, self.relpath))
992 accumulated_paths = []
993 accumulated_length = 0
994 else:
995 accumulated_paths.append(p)
996 accumulated_length += len(p)
997 if accumulated_paths:
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +0000998 RunSVN(command + accumulated_paths,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000999 os.path.join(self._root_dir, self.relpath))
1000
1001 def status(self, options, args, file_list):
1002 """Display status information."""
maruel@chromium.orgedd27d12009-05-01 17:46:56 +00001003 path = os.path.join(self._root_dir, self.relpath)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001004 command = ['status']
1005 command.extend(args)
maruel@chromium.orgedd27d12009-05-01 17:46:56 +00001006 if not os.path.isdir(path):
1007 # svn status won't work if the directory doesn't exist.
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +00001008 print("\n________ couldn't run \'%s\' in \'%s\':\nThe directory "
1009 "does not exist."
1010 % (' '.join(command), path))
maruel@chromium.orgedd27d12009-05-01 17:46:56 +00001011 # There's no file list to retrieve.
1012 else:
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +00001013 RunSVNAndGetFileList(command, path, file_list)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001014
kbr@google.comab318592009-09-04 00:54:55 +00001015 def pack(self, options, args, file_list):
1016 """Generates a patch file which can be applied to the root of the
1017 repository."""
1018 path = os.path.join(self._root_dir, self.relpath)
1019 command = ['diff']
1020 command.extend(args)
1021 # Simple class which tracks which file is being diffed and
1022 # replaces instances of its file name in the original and
1023 # working copy lines of the svn diff output.
1024 class DiffFilterer(object):
1025 index_string = "Index: "
1026 original_prefix = "--- "
1027 working_prefix = "+++ "
1028
1029 def __init__(self, relpath):
1030 # Note that we always use '/' as the path separator to be
1031 # consistent with svn's cygwin-style output on Windows
1032 self._relpath = relpath.replace("\\", "/")
1033 self._current_file = ""
1034 self._replacement_file = ""
1035
1036 def SetCurrentFile(self, file):
1037 self._current_file = file
1038 # Note that we always use '/' as the path separator to be
1039 # consistent with svn's cygwin-style output on Windows
1040 self._replacement_file = self._relpath + '/' + file
1041
1042 def ReplaceAndPrint(self, line):
1043 print(line.replace(self._current_file, self._replacement_file))
1044
1045 def Filter(self, line):
1046 if (line.startswith(self.index_string)):
1047 self.SetCurrentFile(line[len(self.index_string):])
1048 self.ReplaceAndPrint(line)
1049 else:
1050 if (line.startswith(self.original_prefix) or
1051 line.startswith(self.working_prefix)):
1052 self.ReplaceAndPrint(line)
1053 else:
1054 print line
1055
1056 filterer = DiffFilterer(self.relpath)
1057 RunSVNAndFilterOutput(command, path, False, False, filterer.Filter)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001058
1059## GClient implementation.
1060
1061
1062class GClient(object):
1063 """Object that represent a gclient checkout."""
1064
1065 supported_commands = [
kbr@google.comab318592009-09-04 00:54:55 +00001066 'cleanup', 'diff', 'export', 'pack', 'revert', 'status', 'update',
1067 'runhooks'
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001068 ]
1069
1070 def __init__(self, root_dir, options):
1071 self._root_dir = root_dir
1072 self._options = options
1073 self._config_content = None
1074 self._config_dict = {}
1075 self._deps_hooks = []
1076
1077 def SetConfig(self, content):
1078 self._config_dict = {}
1079 self._config_content = content
skylined@chromium.orgdf0032c2009-05-29 10:43:56 +00001080 try:
1081 exec(content, self._config_dict)
1082 except SyntaxError, e:
1083 try:
1084 # Try to construct a human readable error message
1085 error_message = [
1086 'There is a syntax error in your configuration file.',
1087 'Line #%s, character %s:' % (e.lineno, e.offset),
1088 '"%s"' % re.sub(r'[\r\n]*$', '', e.text) ]
1089 except:
1090 # Something went wrong, re-raise the original exception
1091 raise e
1092 else:
1093 # Raise a new exception with the human readable message:
1094 raise Error('\n'.join(error_message))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001095
1096 def SaveConfig(self):
1097 FileWrite(os.path.join(self._root_dir, self._options.config_filename),
1098 self._config_content)
1099
1100 def _LoadConfig(self):
1101 client_source = FileRead(os.path.join(self._root_dir,
1102 self._options.config_filename))
1103 self.SetConfig(client_source)
1104
1105 def ConfigContent(self):
1106 return self._config_content
1107
1108 def GetVar(self, key, default=None):
1109 return self._config_dict.get(key, default)
1110
1111 @staticmethod
1112 def LoadCurrentConfig(options, from_dir=None):
1113 """Searches for and loads a .gclient file relative to the current working
1114 dir.
1115
1116 Returns:
1117 A dict representing the contents of the .gclient file or an empty dict if
1118 the .gclient file doesn't exist.
1119 """
1120 if not from_dir:
1121 from_dir = os.curdir
1122 path = os.path.realpath(from_dir)
maruel@chromium.org0329e672009-05-13 18:41:04 +00001123 while not os.path.exists(os.path.join(path, options.config_filename)):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001124 next = os.path.split(path)
1125 if not next[1]:
1126 return None
1127 path = next[0]
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001128 client = GClient(path, options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001129 client._LoadConfig()
1130 return client
1131
1132 def SetDefaultConfig(self, solution_name, solution_url, safesync_url):
gspencer@google.comdf2d5902009-09-11 22:16:21 +00001133 self.SetConfig(DEFAULT_CLIENT_FILE_TEXT % {
1134 'solution_name': solution_name,
1135 'solution_url': solution_url,
1136 'safesync_url' : safesync_url,
1137 })
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001138
1139 def _SaveEntries(self, entries):
1140 """Creates a .gclient_entries file to record the list of unique checkouts.
1141
1142 The .gclient_entries file lives in the same directory as .gclient.
1143
1144 Args:
1145 entries: A sequence of solution names.
1146 """
1147 text = "entries = [\n"
1148 for entry in entries:
1149 text += " \"%s\",\n" % entry
1150 text += "]\n"
1151 FileWrite(os.path.join(self._root_dir, self._options.entries_filename),
1152 text)
1153
1154 def _ReadEntries(self):
1155 """Read the .gclient_entries file for the given client.
1156
1157 Args:
1158 client: The client for which the entries file should be read.
1159
1160 Returns:
1161 A sequence of solution names, which will be empty if there is the
1162 entries file hasn't been created yet.
1163 """
1164 scope = {}
1165 filename = os.path.join(self._root_dir, self._options.entries_filename)
maruel@chromium.org0329e672009-05-13 18:41:04 +00001166 if not os.path.exists(filename):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001167 return []
1168 exec(FileRead(filename), scope)
1169 return scope["entries"]
1170
1171 class FromImpl:
1172 """Used to implement the From syntax."""
1173
1174 def __init__(self, module_name):
1175 self.module_name = module_name
1176
1177 def __str__(self):
1178 return 'From("%s")' % self.module_name
1179
1180 class _VarImpl:
1181 def __init__(self, custom_vars, local_scope):
1182 self._custom_vars = custom_vars
1183 self._local_scope = local_scope
1184
1185 def Lookup(self, var_name):
1186 """Implements the Var syntax."""
1187 if var_name in self._custom_vars:
1188 return self._custom_vars[var_name]
1189 elif var_name in self._local_scope.get("vars", {}):
1190 return self._local_scope["vars"][var_name]
1191 raise Error("Var is not defined: %s" % var_name)
1192
1193 def _ParseSolutionDeps(self, solution_name, solution_deps_content,
1194 custom_vars):
1195 """Parses the DEPS file for the specified solution.
1196
1197 Args:
1198 solution_name: The name of the solution to query.
1199 solution_deps_content: Content of the DEPS file for the solution
1200 custom_vars: A dict of vars to override any vars defined in the DEPS file.
1201
1202 Returns:
1203 A dict mapping module names (as relative paths) to URLs or an empty
1204 dict if the solution does not have a DEPS file.
1205 """
1206 # Skip empty
1207 if not solution_deps_content:
1208 return {}
1209 # Eval the content
1210 local_scope = {}
1211 var = self._VarImpl(custom_vars, local_scope)
1212 global_scope = {"From": self.FromImpl, "Var": var.Lookup, "deps_os": {}}
1213 exec(solution_deps_content, global_scope, local_scope)
1214 deps = local_scope.get("deps", {})
1215
1216 # load os specific dependencies if defined. these dependencies may
1217 # override or extend the values defined by the 'deps' member.
1218 if "deps_os" in local_scope:
1219 deps_os_choices = {
1220 "win32": "win",
1221 "win": "win",
1222 "cygwin": "win",
1223 "darwin": "mac",
1224 "mac": "mac",
1225 "unix": "unix",
1226 "linux": "unix",
1227 "linux2": "unix",
1228 }
1229
1230 if self._options.deps_os is not None:
1231 deps_to_include = self._options.deps_os.split(",")
1232 if "all" in deps_to_include:
1233 deps_to_include = deps_os_choices.values()
1234 else:
1235 deps_to_include = [deps_os_choices.get(self._options.platform, "unix")]
1236
1237 deps_to_include = set(deps_to_include)
1238 for deps_os_key in deps_to_include:
1239 os_deps = local_scope["deps_os"].get(deps_os_key, {})
1240 if len(deps_to_include) > 1:
1241 # Ignore any overrides when including deps for more than one
1242 # platform, so we collect the broadest set of dependencies available.
1243 # We may end up with the wrong revision of something for our
1244 # platform, but this is the best we can do.
1245 deps.update([x for x in os_deps.items() if not x[0] in deps])
1246 else:
1247 deps.update(os_deps)
1248
1249 if 'hooks' in local_scope:
1250 self._deps_hooks.extend(local_scope['hooks'])
1251
1252 # If use_relative_paths is set in the DEPS file, regenerate
1253 # the dictionary using paths relative to the directory containing
1254 # the DEPS file.
1255 if local_scope.get('use_relative_paths'):
1256 rel_deps = {}
1257 for d, url in deps.items():
1258 # normpath is required to allow DEPS to use .. in their
1259 # dependency local path.
1260 rel_deps[os.path.normpath(os.path.join(solution_name, d))] = url
1261 return rel_deps
1262 else:
1263 return deps
1264
1265 def _ParseAllDeps(self, solution_urls, solution_deps_content):
1266 """Parse the complete list of dependencies for the client.
1267
1268 Args:
1269 solution_urls: A dict mapping module names (as relative paths) to URLs
1270 corresponding to the solutions specified by the client. This parameter
1271 is passed as an optimization.
1272 solution_deps_content: A dict mapping module names to the content
1273 of their DEPS files
1274
1275 Returns:
1276 A dict mapping module names (as relative paths) to URLs corresponding
1277 to the entire set of dependencies to checkout for the given client.
1278
1279 Raises:
1280 Error: If a dependency conflicts with another dependency or of a solution.
1281 """
1282 deps = {}
1283 for solution in self.GetVar("solutions"):
1284 custom_vars = solution.get("custom_vars", {})
1285 solution_deps = self._ParseSolutionDeps(
1286 solution["name"],
1287 solution_deps_content[solution["name"]],
1288 custom_vars)
1289
1290 # If a line is in custom_deps, but not in the solution, we want to append
1291 # this line to the solution.
1292 if "custom_deps" in solution:
1293 for d in solution["custom_deps"]:
1294 if d not in solution_deps:
1295 solution_deps[d] = solution["custom_deps"][d]
1296
1297 for d in solution_deps:
1298 if "custom_deps" in solution and d in solution["custom_deps"]:
1299 # Dependency is overriden.
1300 url = solution["custom_deps"][d]
1301 if url is None:
1302 continue
1303 else:
1304 url = solution_deps[d]
1305 # if we have a From reference dependent on another solution, then
1306 # just skip the From reference. When we pull deps for the solution,
1307 # we will take care of this dependency.
1308 #
1309 # If multiple solutions all have the same From reference, then we
1310 # should only add one to our list of dependencies.
1311 if type(url) != str:
1312 if url.module_name in solution_urls:
1313 # Already parsed.
1314 continue
1315 if d in deps and type(deps[d]) != str:
1316 if url.module_name == deps[d].module_name:
1317 continue
1318 else:
1319 parsed_url = urlparse.urlparse(url)
1320 scheme = parsed_url[0]
1321 if not scheme:
1322 # A relative url. Fetch the real base.
1323 path = parsed_url[2]
1324 if path[0] != "/":
1325 raise Error(
1326 "relative DEPS entry \"%s\" must begin with a slash" % d)
1327 # Create a scm just to query the full url.
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001328 scm = SCMWrapper(solution["url"], self._root_dir, None)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001329 url = scm.FullUrlForRelativeUrl(url)
1330 if d in deps and deps[d] != url:
1331 raise Error(
1332 "Solutions have conflicting versions of dependency \"%s\"" % d)
1333 if d in solution_urls and solution_urls[d] != url:
1334 raise Error(
1335 "Dependency \"%s\" conflicts with specified solution" % d)
1336 # Grab the dependency.
1337 deps[d] = url
1338 return deps
1339
phajdan.jr@chromium.org71b40682009-07-31 23:40:09 +00001340 def _RunHookAction(self, hook_dict, matching_file_list):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001341 """Runs the action from a single hook.
1342 """
1343 command = hook_dict['action'][:]
1344 if command[0] == 'python':
1345 # If the hook specified "python" as the first item, the action is a
1346 # Python script. Run it by starting a new copy of the same
1347 # interpreter.
1348 command[0] = sys.executable
1349
phajdan.jr@chromium.org71b40682009-07-31 23:40:09 +00001350 if '$matching_files' in command:
phajdan.jr@chromium.org68f2e092009-08-06 17:05:35 +00001351 splice_index = command.index('$matching_files')
1352 command[splice_index:splice_index + 1] = matching_file_list
phajdan.jr@chromium.org71b40682009-07-31 23:40:09 +00001353
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001354 # Use a discrete exit status code of 2 to indicate that a hook action
1355 # failed. Users of this script may wish to treat hook action failures
1356 # differently from VC failures.
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +00001357 SubprocessCall(command, self._root_dir, fail_status=2)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001358
1359 def _RunHooks(self, command, file_list, is_using_git):
1360 """Evaluates all hooks, running actions as needed.
1361 """
1362 # Hooks only run for these command types.
1363 if not command in ('update', 'revert', 'runhooks'):
1364 return
1365
evan@chromium.org67820ef2009-07-27 17:23:00 +00001366 # Hooks only run when --nohooks is not specified
1367 if self._options.nohooks:
1368 return
1369
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001370 # Get any hooks from the .gclient file.
1371 hooks = self.GetVar("hooks", [])
1372 # Add any hooks found in DEPS files.
1373 hooks.extend(self._deps_hooks)
1374
1375 # If "--force" was specified, run all hooks regardless of what files have
1376 # changed. If the user is using git, then we don't know what files have
1377 # changed so we always run all hooks.
1378 if self._options.force or is_using_git:
1379 for hook_dict in hooks:
phajdan.jr@chromium.org71b40682009-07-31 23:40:09 +00001380 self._RunHookAction(hook_dict, [])
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001381 return
1382
1383 # Run hooks on the basis of whether the files from the gclient operation
1384 # match each hook's pattern.
1385 for hook_dict in hooks:
1386 pattern = re.compile(hook_dict['pattern'])
phajdan.jr@chromium.org71b40682009-07-31 23:40:09 +00001387 matching_file_list = [file for file in file_list if pattern.search(file)]
1388 if matching_file_list:
1389 self._RunHookAction(hook_dict, matching_file_list)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001390
1391 def RunOnDeps(self, command, args):
1392 """Runs a command on each dependency in a client and its dependencies.
1393
1394 The module's dependencies are specified in its top-level DEPS files.
1395
1396 Args:
1397 command: The command to use (e.g., 'status' or 'diff')
1398 args: list of str - extra arguments to add to the command line.
1399
1400 Raises:
1401 Error: If the client has conflicting entries.
1402 """
1403 if not command in self.supported_commands:
1404 raise Error("'%s' is an unsupported command" % command)
1405
1406 # Check for revision overrides.
1407 revision_overrides = {}
1408 for revision in self._options.revisions:
1409 if revision.find("@") == -1:
1410 raise Error(
1411 "Specify the full dependency when specifying a revision number.")
1412 revision_elem = revision.split("@")
1413 # Disallow conflicting revs
1414 if revision_overrides.has_key(revision_elem[0]) and \
1415 revision_overrides[revision_elem[0]] != revision_elem[1]:
1416 raise Error(
1417 "Conflicting revision numbers specified.")
1418 revision_overrides[revision_elem[0]] = revision_elem[1]
1419
1420 solutions = self.GetVar("solutions")
1421 if not solutions:
1422 raise Error("No solution specified")
1423
1424 # When running runhooks --force, there's no need to consult the SCM.
1425 # All known hooks are expected to run unconditionally regardless of working
1426 # copy state, so skip the SCM status check.
1427 run_scm = not (command == 'runhooks' and self._options.force)
1428
1429 entries = {}
1430 entries_deps_content = {}
1431 file_list = []
1432 # Run on the base solutions first.
1433 for solution in solutions:
1434 name = solution["name"]
gspencer@google.comdf2d5902009-09-11 22:16:21 +00001435 deps_file = solution.get("deps_file", self._options.deps_file)
1436 if '/' in deps_file or '\\' in deps_file:
1437 raise Error("deps_file name must not be a path, just a filename.")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001438 if name in entries:
1439 raise Error("solution %s specified more than once" % name)
1440 url = solution["url"]
1441 entries[name] = url
1442 if run_scm:
1443 self._options.revision = revision_overrides.get(name)
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001444 scm = SCMWrapper(url, self._root_dir, name)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001445 scm.RunCommand(command, self._options, args, file_list)
phajdan.jr@chromium.orgd83b2b22009-08-11 15:30:55 +00001446 file_list = [os.path.join(name, file.strip()) for file in file_list]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001447 self._options.revision = None
1448 try:
1449 deps_content = FileRead(os.path.join(self._root_dir, name,
gspencer@google.comdf2d5902009-09-11 22:16:21 +00001450 deps_file))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001451 except IOError, e:
1452 if e.errno != errno.ENOENT:
1453 raise
1454 deps_content = ""
1455 entries_deps_content[name] = deps_content
1456
1457 # Process the dependencies next (sort alphanumerically to ensure that
1458 # containing directories get populated first and for readability)
1459 deps = self._ParseAllDeps(entries, entries_deps_content)
1460 deps_to_process = deps.keys()
1461 deps_to_process.sort()
1462
1463 # First pass for direct dependencies.
1464 for d in deps_to_process:
1465 if type(deps[d]) == str:
1466 url = deps[d]
1467 entries[d] = url
1468 if run_scm:
1469 self._options.revision = revision_overrides.get(d)
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001470 scm = SCMWrapper(url, self._root_dir, d)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001471 scm.RunCommand(command, self._options, args, file_list)
1472 self._options.revision = None
1473
1474 # Second pass for inherited deps (via the From keyword)
1475 for d in deps_to_process:
1476 if type(deps[d]) != str:
1477 sub_deps = self._ParseSolutionDeps(
1478 deps[d].module_name,
1479 FileRead(os.path.join(self._root_dir,
1480 deps[d].module_name,
1481 self._options.deps_file)),
1482 {})
1483 url = sub_deps[d]
1484 entries[d] = url
1485 if run_scm:
1486 self._options.revision = revision_overrides.get(d)
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001487 scm = SCMWrapper(url, self._root_dir, d)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001488 scm.RunCommand(command, self._options, args, file_list)
1489 self._options.revision = None
gspencer@google.comdf2d5902009-09-11 22:16:21 +00001490
phajdan.jr@chromium.orgd83b2b22009-08-11 15:30:55 +00001491 # Convert all absolute paths to relative.
1492 for i in range(len(file_list)):
1493 # TODO(phajdan.jr): We should know exactly when the paths are absolute.
1494 # It depends on the command being executed (like runhooks vs sync).
1495 if not os.path.isabs(file_list[i]):
1496 continue
1497
1498 prefix = os.path.commonprefix([self._root_dir.lower(),
1499 file_list[i].lower()])
1500 file_list[i] = file_list[i][len(prefix):]
1501
1502 # Strip any leading path separators.
1503 while file_list[i].startswith('\\') or file_list[i].startswith('/'):
1504 file_list[i] = file_list[i][1:]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001505
1506 is_using_git = IsUsingGit(self._root_dir, entries.keys())
1507 self._RunHooks(command, file_list, is_using_git)
1508
1509 if command == 'update':
ajwong@chromium.orgcdcee802009-06-23 15:30:42 +00001510 # Notify the user if there is an orphaned entry in their working copy.
1511 # Only delete the directory if there are no changes in it, and
1512 # delete_unversioned_trees is set to true.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001513 prev_entries = self._ReadEntries()
1514 for entry in prev_entries:
maruel@chromium.orgc5e9aec2009-08-03 18:25:56 +00001515 # Fix path separator on Windows.
1516 entry_fixed = entry.replace('/', os.path.sep)
1517 e_dir = os.path.join(self._root_dir, entry_fixed)
1518 # Use entry and not entry_fixed there.
maruel@chromium.org0329e672009-05-13 18:41:04 +00001519 if entry not in entries and os.path.exists(e_dir):
ajwong@chromium.org8399dc02009-06-23 21:36:25 +00001520 if not self._options.delete_unversioned_trees or \
1521 CaptureSVNStatus(e_dir):
maruel@chromium.orgc5e9aec2009-08-03 18:25:56 +00001522 # There are modified files in this entry. Keep warning until
1523 # removed.
1524 entries[entry] = None
1525 print(("\nWARNING: \"%s\" is no longer part of this client. "
1526 "It is recommended that you manually remove it.\n") %
1527 entry_fixed)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001528 else:
1529 # Delete the entry
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +00001530 print("\n________ deleting \'%s\' " +
maruel@chromium.orgc5e9aec2009-08-03 18:25:56 +00001531 "in \'%s\'") % (entry_fixed, self._root_dir)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001532 RemoveDirectory(e_dir)
1533 # record the current list of entries for next time
1534 self._SaveEntries(entries)
1535
1536 def PrintRevInfo(self):
1537 """Output revision info mapping for the client and its dependencies. This
1538 allows the capture of a overall "revision" for the source tree that can
1539 be used to reproduce the same tree in the future. The actual output
1540 contains enough information (source paths, svn server urls and revisions)
1541 that it can be used either to generate external svn commands (without
1542 gclient) or as input to gclient's --rev option (with some massaging of
1543 the data).
1544
1545 NOTE: Unlike RunOnDeps this does not require a local checkout and is run
1546 on the Pulse master. It MUST NOT execute hooks.
1547
1548 Raises:
1549 Error: If the client has conflicting entries.
1550 """
1551 # Check for revision overrides.
1552 revision_overrides = {}
1553 for revision in self._options.revisions:
1554 if revision.find("@") < 0:
1555 raise Error(
1556 "Specify the full dependency when specifying a revision number.")
1557 revision_elem = revision.split("@")
1558 # Disallow conflicting revs
1559 if revision_overrides.has_key(revision_elem[0]) and \
1560 revision_overrides[revision_elem[0]] != revision_elem[1]:
1561 raise Error(
1562 "Conflicting revision numbers specified.")
1563 revision_overrides[revision_elem[0]] = revision_elem[1]
1564
1565 solutions = self.GetVar("solutions")
1566 if not solutions:
1567 raise Error("No solution specified")
1568
1569 entries = {}
1570 entries_deps_content = {}
1571
1572 # Inner helper to generate base url and rev tuple (including honoring
1573 # |revision_overrides|)
1574 def GetURLAndRev(name, original_url):
1575 if original_url.find("@") < 0:
1576 if revision_overrides.has_key(name):
1577 return (original_url, int(revision_overrides[name]))
1578 else:
1579 # TODO(aharper): SVN/SCMWrapper cleanup (non-local commandset)
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +00001580 return (original_url, CaptureSVNHeadRevision(original_url))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001581 else:
1582 url_components = original_url.split("@")
1583 if revision_overrides.has_key(name):
1584 return (url_components[0], int(revision_overrides[name]))
1585 else:
1586 return (url_components[0], int(url_components[1]))
1587
1588 # Run on the base solutions first.
1589 for solution in solutions:
1590 name = solution["name"]
1591 if name in entries:
1592 raise Error("solution %s specified more than once" % name)
1593 (url, rev) = GetURLAndRev(name, solution["url"])
1594 entries[name] = "%s@%d" % (url, rev)
1595 # TODO(aharper): SVN/SCMWrapper cleanup (non-local commandset)
1596 entries_deps_content[name] = CaptureSVN(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001597 ["cat",
1598 "%s/%s@%d" % (url,
1599 self._options.deps_file,
1600 rev)],
1601 os.getcwd())
1602
1603 # Process the dependencies next (sort alphanumerically to ensure that
1604 # containing directories get populated first and for readability)
1605 deps = self._ParseAllDeps(entries, entries_deps_content)
1606 deps_to_process = deps.keys()
1607 deps_to_process.sort()
1608
1609 # First pass for direct dependencies.
1610 for d in deps_to_process:
1611 if type(deps[d]) == str:
1612 (url, rev) = GetURLAndRev(d, deps[d])
1613 entries[d] = "%s@%d" % (url, rev)
1614
1615 # Second pass for inherited deps (via the From keyword)
1616 for d in deps_to_process:
1617 if type(deps[d]) != str:
1618 deps_parent_url = entries[deps[d].module_name]
1619 if deps_parent_url.find("@") < 0:
1620 raise Error("From %s missing revisioned url" % deps[d].module_name)
1621 deps_parent_url_components = deps_parent_url.split("@")
1622 # TODO(aharper): SVN/SCMWrapper cleanup (non-local commandset)
1623 deps_parent_content = CaptureSVN(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001624 ["cat",
1625 "%s/%s@%s" % (deps_parent_url_components[0],
1626 self._options.deps_file,
1627 deps_parent_url_components[1])],
1628 os.getcwd())
1629 sub_deps = self._ParseSolutionDeps(
1630 deps[d].module_name,
1631 FileRead(os.path.join(self._root_dir,
1632 deps[d].module_name,
1633 self._options.deps_file)),
1634 {})
1635 (url, rev) = GetURLAndRev(d, sub_deps[d])
1636 entries[d] = "%s@%d" % (url, rev)
maruel@chromium.org57e893e2009-08-19 18:12:09 +00001637 print(";\n\n".join(["%s: %s" % (x, entries[x])
1638 for x in sorted(entries.keys())]))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001639
1640
1641## gclient commands.
1642
1643
1644def DoCleanup(options, args):
1645 """Handle the cleanup subcommand.
1646
1647 Raises:
1648 Error: if client isn't configured properly.
1649 """
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001650 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001651 if not client:
1652 raise Error("client not configured; see 'gclient config'")
1653 if options.verbose:
1654 # Print out the .gclient file. This is longer than if we just printed the
1655 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +00001656 print(client.ConfigContent())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001657 options.verbose = True
1658 return client.RunOnDeps('cleanup', args)
1659
1660
1661def DoConfig(options, args):
1662 """Handle the config subcommand.
1663
1664 Args:
1665 options: If options.spec set, a string providing contents of config file.
1666 args: The command line args. If spec is not set,
1667 then args[0] is a string URL to get for config file.
1668
1669 Raises:
1670 Error: on usage error
1671 """
1672 if len(args) < 1 and not options.spec:
1673 raise Error("required argument missing; see 'gclient help config'")
maruel@chromium.org0329e672009-05-13 18:41:04 +00001674 if os.path.exists(options.config_filename):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001675 raise Error("%s file already exists in the current directory" %
1676 options.config_filename)
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001677 client = GClient('.', options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001678 if options.spec:
1679 client.SetConfig(options.spec)
1680 else:
1681 # TODO(darin): it would be nice to be able to specify an alternate relpath
1682 # for the given URL.
maruel@chromium.org1ab7ffc2009-06-03 17:21:37 +00001683 base_url = args[0].rstrip('/')
1684 name = base_url.split("/")[-1]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001685 safesync_url = ""
1686 if len(args) > 1:
1687 safesync_url = args[1]
1688 client.SetDefaultConfig(name, base_url, safesync_url)
1689 client.SaveConfig()
1690
1691
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +00001692def DoExport(options, args):
1693 """Handle the export subcommand.
phajdan.jr@chromium.org71b40682009-07-31 23:40:09 +00001694
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +00001695 Raises:
1696 Error: on usage error
1697 """
1698 if len(args) != 1:
1699 raise Error("Need directory name")
1700 client = GClient.LoadCurrentConfig(options)
1701
1702 if not client:
1703 raise Error("client not configured; see 'gclient config'")
1704
1705 if options.verbose:
1706 # Print out the .gclient file. This is longer than if we just printed the
1707 # client dict, but more legible, and it might contain helpful comments.
1708 print(client.ConfigContent())
1709 return client.RunOnDeps('export', args)
1710
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001711def DoHelp(options, args):
1712 """Handle the help subcommand giving help for another subcommand.
1713
1714 Raises:
1715 Error: if the command is unknown.
1716 """
1717 if len(args) == 1 and args[0] in COMMAND_USAGE_TEXT:
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +00001718 print(COMMAND_USAGE_TEXT[args[0]])
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001719 else:
1720 raise Error("unknown subcommand '%s'; see 'gclient help'" % args[0])
1721
1722
kbr@google.comab318592009-09-04 00:54:55 +00001723def DoPack(options, args):
1724 """Handle the pack subcommand.
1725
1726 Raises:
1727 Error: if client isn't configured properly.
1728 """
1729 client = GClient.LoadCurrentConfig(options)
1730 if not client:
1731 raise Error("client not configured; see 'gclient config'")
1732 if options.verbose:
1733 # Print out the .gclient file. This is longer than if we just printed the
1734 # client dict, but more legible, and it might contain helpful comments.
1735 print(client.ConfigContent())
1736 options.verbose = True
1737 return client.RunOnDeps('pack', args)
1738
1739
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001740def DoStatus(options, args):
1741 """Handle the status subcommand.
1742
1743 Raises:
1744 Error: if client isn't configured properly.
1745 """
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001746 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001747 if not client:
1748 raise Error("client not configured; see 'gclient config'")
1749 if options.verbose:
1750 # Print out the .gclient file. This is longer than if we just printed the
1751 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +00001752 print(client.ConfigContent())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001753 options.verbose = True
1754 return client.RunOnDeps('status', args)
1755
1756
1757def DoUpdate(options, args):
1758 """Handle the update and sync subcommands.
1759
1760 Raises:
1761 Error: if client isn't configured properly.
1762 """
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001763 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001764
1765 if not client:
1766 raise Error("client not configured; see 'gclient config'")
1767
1768 if not options.head:
1769 solutions = client.GetVar('solutions')
1770 if solutions:
1771 for s in solutions:
1772 if s.get('safesync_url', ''):
1773 # rip through revisions and make sure we're not over-riding
1774 # something that was explicitly passed
1775 has_key = False
1776 for r in options.revisions:
1777 if r.split('@')[0] == s['name']:
1778 has_key = True
1779 break
1780
1781 if not has_key:
1782 handle = urllib.urlopen(s['safesync_url'])
1783 rev = handle.read().strip()
1784 handle.close()
1785 if len(rev):
1786 options.revisions.append(s['name']+'@'+rev)
1787
1788 if options.verbose:
1789 # Print out the .gclient file. This is longer than if we just printed the
1790 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +00001791 print(client.ConfigContent())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001792 return client.RunOnDeps('update', args)
1793
1794
1795def DoDiff(options, args):
1796 """Handle the diff subcommand.
1797
1798 Raises:
1799 Error: if client isn't configured properly.
1800 """
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001801 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001802 if not client:
1803 raise Error("client not configured; see 'gclient config'")
1804 if options.verbose:
1805 # Print out the .gclient file. This is longer than if we just printed the
1806 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +00001807 print(client.ConfigContent())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001808 options.verbose = True
1809 return client.RunOnDeps('diff', args)
1810
1811
1812def DoRevert(options, args):
1813 """Handle the revert subcommand.
1814
1815 Raises:
1816 Error: if client isn't configured properly.
1817 """
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001818 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001819 if not client:
1820 raise Error("client not configured; see 'gclient config'")
1821 return client.RunOnDeps('revert', args)
1822
1823
1824def DoRunHooks(options, args):
1825 """Handle the runhooks subcommand.
1826
1827 Raises:
1828 Error: if client isn't configured properly.
1829 """
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001830 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001831 if not client:
1832 raise Error("client not configured; see 'gclient config'")
1833 if options.verbose:
1834 # Print out the .gclient file. This is longer than if we just printed the
1835 # client dict, but more legible, and it might contain helpful comments.
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +00001836 print(client.ConfigContent())
maruel@chromium.org5df6a462009-08-28 18:52:26 +00001837 options.force = True
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001838 return client.RunOnDeps('runhooks', args)
1839
1840
1841def DoRevInfo(options, args):
1842 """Handle the revinfo subcommand.
1843
1844 Raises:
1845 Error: if client isn't configured properly.
1846 """
maruel@chromium.org2806acc2009-05-15 12:33:34 +00001847 client = GClient.LoadCurrentConfig(options)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001848 if not client:
1849 raise Error("client not configured; see 'gclient config'")
1850 client.PrintRevInfo()
1851
1852
1853gclient_command_map = {
1854 "cleanup": DoCleanup,
1855 "config": DoConfig,
1856 "diff": DoDiff,
phajdan.jr@chromium.org644aa0c2009-07-17 20:20:41 +00001857 "export": DoExport,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001858 "help": DoHelp,
kbr@google.comab318592009-09-04 00:54:55 +00001859 "pack": DoPack,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001860 "status": DoStatus,
1861 "sync": DoUpdate,
1862 "update": DoUpdate,
1863 "revert": DoRevert,
1864 "runhooks": DoRunHooks,
1865 "revinfo" : DoRevInfo,
1866}
1867
1868
1869def DispatchCommand(command, options, args, command_map=None):
1870 """Dispatches the appropriate subcommand based on command line arguments."""
1871 if command_map is None:
1872 command_map = gclient_command_map
1873
1874 if command in command_map:
1875 return command_map[command](options, args)
1876 else:
1877 raise Error("unknown subcommand '%s'; see 'gclient help'" % command)
1878
1879
1880def Main(argv):
1881 """Parse command line arguments and dispatch command."""
1882
1883 option_parser = optparse.OptionParser(usage=DEFAULT_USAGE_TEXT,
1884 version=__version__)
1885 option_parser.disable_interspersed_args()
1886 option_parser.add_option("", "--force", action="store_true", default=False,
1887 help=("(update/sync only) force update even "
1888 "for modules which haven't changed"))
evan@chromium.org67820ef2009-07-27 17:23:00 +00001889 option_parser.add_option("", "--nohooks", action="store_true", default=False,
1890 help=("(update/sync/revert only) prevent the hooks from "
1891 "running"))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001892 option_parser.add_option("", "--revision", action="append", dest="revisions",
1893 metavar="REV", default=[],
1894 help=("(update/sync only) sync to a specific "
1895 "revision, can be used multiple times for "
1896 "each solution, e.g. --revision=src@123, "
1897 "--revision=internal@32"))
1898 option_parser.add_option("", "--deps", default=None, dest="deps_os",
1899 metavar="OS_LIST",
1900 help=("(update/sync only) sync deps for the "
1901 "specified (comma-separated) platform(s); "
1902 "'all' will sync all platforms"))
1903 option_parser.add_option("", "--spec", default=None,
1904 help=("(config only) create a gclient file "
1905 "containing the provided string"))
1906 option_parser.add_option("", "--verbose", action="store_true", default=False,
1907 help="produce additional output for diagnostics")
1908 option_parser.add_option("", "--manually_grab_svn_rev", action="store_true",
1909 default=False,
1910 help="Skip svn up whenever possible by requesting "
1911 "actual HEAD revision from the repository")
1912 option_parser.add_option("", "--head", action="store_true", default=False,
1913 help=("skips any safesync_urls specified in "
1914 "configured solutions"))
ajwong@chromium.orgcdcee802009-06-23 15:30:42 +00001915 option_parser.add_option("", "--delete_unversioned_trees",
1916 action="store_true", default=False,
1917 help=("on update, delete any unexpected "
1918 "unversioned trees that are in the checkout"))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001919
1920 if len(argv) < 2:
1921 # Users don't need to be told to use the 'help' command.
1922 option_parser.print_help()
1923 return 1
1924 # Add manual support for --version as first argument.
1925 if argv[1] == '--version':
1926 option_parser.print_version()
1927 return 0
1928
1929 # Add manual support for --help as first argument.
1930 if argv[1] == '--help':
1931 argv[1] = 'help'
1932
1933 command = argv[1]
1934 options, args = option_parser.parse_args(argv[2:])
1935
1936 if len(argv) < 3 and command == "help":
1937 option_parser.print_help()
1938 return 0
1939
1940 # Files used for configuration and state saving.
1941 options.config_filename = os.environ.get("GCLIENT_FILE", ".gclient")
1942 options.entries_filename = ".gclient_entries"
1943 options.deps_file = "DEPS"
1944
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001945 options.platform = sys.platform
1946 return DispatchCommand(command, options, args)
1947
1948
1949if "__main__" == __name__:
1950 try:
1951 result = Main(sys.argv)
1952 except Error, e:
maruel@chromium.orgdf7a3132009-05-12 17:49:49 +00001953 print >> sys.stderr, "Error: %s" % str(e)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001954 result = 1
1955 sys.exit(result)
1956
1957# vim: ts=2:sw=2:tw=80:et: